claude-baseline-1752110594
This commit is contained in:
parent
5620133b8b
commit
a488a0930e
|
@ -29,3 +29,9 @@ After reloading hooks module - this should create debug logs!
|
|||
Testing after manual baseline creation - this should work now!
|
||||
|
||||
After manually initializing stash session - test the browsing commands!
|
||||
|
||||
Testing unified.nvim integration - this should create a Claude stash that we can view in unified mode with fine-grained hunk accept/reject functionality!
|
||||
|
||||
After nvim reload - testing unified view with <leader>du to see fine-grained hunk staging!
|
||||
|
||||
Testing inline diff viewer - Claude's changes should appear directly in this buffer with virtual text showing additions and highlights for deletions!
|
||||
|
|
|
@ -10,5 +10,6 @@ return {
|
|||
'nvim-telescope/telescope.nvim', -- For agent picker
|
||||
'tpope/vim-fugitive', -- Already installed, for diffs
|
||||
'sindrets/diffview.nvim', -- For advanced diff viewing
|
||||
'axkirillov/unified.nvim', -- For unified diff view with fine-grained hunk staging
|
||||
},
|
||||
}
|
|
@ -166,6 +166,13 @@ function M.setup_keybindings()
|
|||
vim.keymap.set('n', '<leader>dh', M.browse_claude_stashes, { desc = 'Browse Claude stash history' })
|
||||
vim.keymap.set('n', '<leader>dp', M.previous_stash, { desc = 'View previous Claude stash' })
|
||||
vim.keymap.set('n', '<leader>dn', M.next_stash, { desc = 'View next Claude stash' })
|
||||
|
||||
-- Unified view
|
||||
vim.keymap.set('n', '<leader>du', M.open_unified_view, { desc = 'Open Claude diff in unified view' })
|
||||
|
||||
-- Hunk operations
|
||||
vim.keymap.set('n', '<leader>dka', M.accept_hunk_at_cursor, { desc = 'Accept Claude hunk at cursor' })
|
||||
vim.keymap.set('n', '<leader>dkr', M.reject_hunk_at_cursor, { desc = 'Reject Claude hunk at cursor' })
|
||||
end
|
||||
|
||||
-- Open diffview for current review
|
||||
|
@ -580,4 +587,269 @@ function M.telescope_claude_stashes()
|
|||
}):find()
|
||||
end
|
||||
|
||||
-- Generate combined patch from all Claude stashes
|
||||
function M.generate_claude_patch()
|
||||
if not M.current_review or not M.current_review.is_stash_based then
|
||||
vim.notify('No Claude stash session active', vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
local utils = require('nvim-claude.utils')
|
||||
local baseline_ref = M.current_review.baseline_ref
|
||||
|
||||
-- Generate diff from baseline to current working directory
|
||||
local cmd = string.format('git diff %s', baseline_ref)
|
||||
local patch, err = utils.exec(cmd)
|
||||
|
||||
if err then
|
||||
vim.notify('Failed to generate patch: ' .. err, vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
return patch
|
||||
end
|
||||
|
||||
-- Open unified view for Claude changes
|
||||
function M.open_unified_view()
|
||||
if not M.current_review then
|
||||
-- Try to recover stash-based session from baseline
|
||||
local utils = require('nvim-claude.utils')
|
||||
local baseline_ref = utils.read_file('/tmp/claude-baseline-commit')
|
||||
|
||||
-- If no baseline file, but we have Claude stashes, use HEAD as baseline
|
||||
local claude_stashes = M.get_claude_stashes()
|
||||
if claude_stashes and #claude_stashes > 0 then
|
||||
if not baseline_ref or baseline_ref == '' then
|
||||
baseline_ref = 'HEAD'
|
||||
vim.notify('No baseline found, using HEAD as baseline', vim.log.levels.INFO)
|
||||
else
|
||||
baseline_ref = baseline_ref:gsub('%s+', '')
|
||||
end
|
||||
|
||||
M.current_review = {
|
||||
baseline_ref = baseline_ref,
|
||||
timestamp = os.time(),
|
||||
claude_stashes = claude_stashes,
|
||||
current_stash_index = 0,
|
||||
is_stash_based = true
|
||||
}
|
||||
vim.notify(string.format('Recovered Claude stash session with %d stashes', #claude_stashes), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
if not M.current_review then
|
||||
vim.notify('No active review session', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if unified.nvim is available and load it
|
||||
local ok, unified = pcall(require, 'unified')
|
||||
if not ok then
|
||||
vim.notify('unified.nvim not available, falling back to diffview', vim.log.levels.WARN)
|
||||
M.open_diffview()
|
||||
return
|
||||
end
|
||||
|
||||
-- Ensure unified.nvim is set up
|
||||
pcall(unified.setup, {})
|
||||
|
||||
-- Use unified.nvim to show diff against baseline
|
||||
local baseline_ref = M.current_review.baseline_ref
|
||||
|
||||
-- Try the command with pcall to catch errors
|
||||
local cmd_ok, cmd_err = pcall(function()
|
||||
vim.cmd('Unified ' .. baseline_ref)
|
||||
end)
|
||||
|
||||
if not cmd_ok then
|
||||
vim.notify('Unified command failed: ' .. tostring(cmd_err) .. ', falling back to diffview', vim.log.levels.WARN)
|
||||
M.open_diffview()
|
||||
return
|
||||
end
|
||||
|
||||
vim.notify('Claude unified diff opened. Use ]h/[h to navigate hunks', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
|
||||
-- Accept hunk at cursor position
|
||||
function M.accept_hunk_at_cursor()
|
||||
-- Get current buffer and check if we're in a diff view
|
||||
local bufname = vim.api.nvim_buf_get_name(0)
|
||||
local filetype = vim.bo.filetype
|
||||
|
||||
-- Check for various diff view types
|
||||
local is_diff_view = bufname:match('diffview://') or
|
||||
bufname:match('Claude Unified Diff') or
|
||||
filetype == 'diff' or
|
||||
filetype == 'git'
|
||||
|
||||
if not is_diff_view then
|
||||
vim.notify('This command only works in diff views', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get current file and line from cursor position
|
||||
local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
|
||||
-- Parse diff to find current hunk
|
||||
local hunk_info = M.find_hunk_at_line(lines, cursor_line)
|
||||
if not hunk_info then
|
||||
vim.notify('No hunk found at cursor position', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Apply the hunk
|
||||
M.apply_hunk(hunk_info)
|
||||
end
|
||||
|
||||
-- Reject hunk at cursor position
|
||||
function M.reject_hunk_at_cursor()
|
||||
-- Get current buffer and check if we're in a diff view
|
||||
local bufname = vim.api.nvim_buf_get_name(0)
|
||||
local filetype = vim.bo.filetype
|
||||
|
||||
-- Check for various diff view types
|
||||
local is_diff_view = bufname:match('diffview://') or
|
||||
bufname:match('Claude Unified Diff') or
|
||||
filetype == 'diff' or
|
||||
filetype == 'git'
|
||||
|
||||
if not is_diff_view then
|
||||
vim.notify('This command only works in diff views', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get current file and line from cursor position
|
||||
local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
|
||||
-- Parse diff to find current hunk
|
||||
local hunk_info = M.find_hunk_at_line(lines, cursor_line)
|
||||
if not hunk_info then
|
||||
vim.notify('No hunk found at cursor position', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
vim.notify(string.format('Rejected hunk in %s at lines %d-%d', hunk_info.file, hunk_info.old_start, hunk_info.old_start + hunk_info.old_count - 1), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Find hunk information at given line in diff buffer
|
||||
function M.find_hunk_at_line(lines, target_line)
|
||||
local current_file = nil
|
||||
local in_hunk = false
|
||||
local hunk_start_line = nil
|
||||
local hunk_lines = {}
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
-- File header
|
||||
if line:match('^diff %-%-git') or line:match('^diff %-%-cc') then
|
||||
current_file = line:match('b/(.+)$')
|
||||
elseif line:match('^%+%+%+ b/(.+)') then
|
||||
current_file = line:match('^%+%+%+ b/(.+)')
|
||||
end
|
||||
|
||||
-- Hunk header
|
||||
if line:match('^@@') then
|
||||
-- If we were in a hunk that included target line, return it
|
||||
if in_hunk and hunk_start_line and target_line >= hunk_start_line and target_line < i then
|
||||
return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line)
|
||||
end
|
||||
|
||||
-- Start new hunk
|
||||
in_hunk = true
|
||||
hunk_start_line = i
|
||||
hunk_lines = {line}
|
||||
elseif in_hunk then
|
||||
-- Collect hunk lines
|
||||
if line:match('^[%+%-%s]') then
|
||||
table.insert(hunk_lines, line)
|
||||
else
|
||||
-- End of hunk
|
||||
if hunk_start_line and target_line >= hunk_start_line and target_line < i then
|
||||
return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line)
|
||||
end
|
||||
in_hunk = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check last hunk
|
||||
if in_hunk and hunk_start_line and target_line >= hunk_start_line then
|
||||
return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Parse hunk information from diff lines
|
||||
function M.parse_hunk_info(hunk_lines, file, start_line)
|
||||
if #hunk_lines == 0 then return nil end
|
||||
|
||||
local header = hunk_lines[1]
|
||||
local old_start, old_count, new_start, new_count = header:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
|
||||
|
||||
if not old_start then return nil end
|
||||
|
||||
return {
|
||||
file = file,
|
||||
old_start = tonumber(old_start),
|
||||
old_count = tonumber(old_count) or 1,
|
||||
new_start = tonumber(new_start),
|
||||
new_count = tonumber(new_count) or 1,
|
||||
lines = hunk_lines,
|
||||
buffer_start_line = start_line
|
||||
}
|
||||
end
|
||||
|
||||
-- Apply a specific hunk to the working directory
|
||||
function M.apply_hunk(hunk_info)
|
||||
local utils = require('nvim-claude.utils')
|
||||
|
||||
-- Create a patch with just this hunk
|
||||
local patch_lines = {
|
||||
'diff --git a/' .. hunk_info.file .. ' b/' .. hunk_info.file,
|
||||
'index 0000000..0000000 100644',
|
||||
'--- a/' .. hunk_info.file,
|
||||
'+++ b/' .. hunk_info.file
|
||||
}
|
||||
|
||||
-- Add hunk lines
|
||||
for _, line in ipairs(hunk_info.lines) do
|
||||
table.insert(patch_lines, line)
|
||||
end
|
||||
|
||||
local patch_content = table.concat(patch_lines, '\n')
|
||||
|
||||
-- Write patch to temp file
|
||||
local temp_patch = '/tmp/claude-hunk-patch.diff'
|
||||
utils.write_file(temp_patch, patch_content)
|
||||
|
||||
-- Apply the patch
|
||||
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 cmd = string.format('cd "%s" && git apply --cached "%s"', git_root, temp_patch)
|
||||
local result, err = utils.exec(cmd)
|
||||
|
||||
if err then
|
||||
-- Try without --cached
|
||||
cmd = string.format('cd "%s" && git apply "%s"', git_root, temp_patch)
|
||||
result, err = utils.exec(cmd)
|
||||
|
||||
if err then
|
||||
vim.notify('Failed to apply hunk: ' .. err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
vim.notify(string.format('Applied hunk to %s', hunk_info.file), vim.log.levels.INFO)
|
||||
|
||||
-- Refresh the buffer if it's open
|
||||
vim.cmd('checktime')
|
||||
end
|
||||
|
||||
return M
|
|
@ -116,6 +116,15 @@ function M.post_tool_use_hook()
|
|||
return
|
||||
end
|
||||
|
||||
-- Get list of modified files
|
||||
local modified_files = {}
|
||||
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)
|
||||
end
|
||||
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)
|
||||
|
@ -136,6 +145,47 @@ function M.post_tool_use_hook()
|
|||
baseline_ref = baseline_ref:gsub('%s+', '') -- trim whitespace
|
||||
end
|
||||
|
||||
-- Check if any modified files are currently open in buffers
|
||||
local inline_diff = require('nvim-claude.inline-diff')
|
||||
local opened_inline = false
|
||||
|
||||
for _, file in ipairs(modified_files) do
|
||||
local full_path = git_root .. '/' .. file
|
||||
|
||||
-- Find buffer with this file
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
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 (from baseline)
|
||||
local baseline_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, baseline_ref or 'HEAD', file)
|
||||
local original_content, orig_err = utils.exec(baseline_cmd)
|
||||
|
||||
if not orig_err and 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)
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if opened_inline then break end
|
||||
end
|
||||
|
||||
-- If no inline diff was shown, fall back to regular diff review
|
||||
if not opened_inline then
|
||||
-- Trigger diff review - show Claude stashes against baseline
|
||||
local ok, diff_review = pcall(require, 'nvim-claude.diff-review')
|
||||
if ok then
|
||||
|
@ -143,6 +193,7 @@ function M.post_tool_use_hook()
|
|||
else
|
||||
vim.notify('Diff review module not available: ' .. tostring(diff_review), vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
else
|
||||
vim.notify('Failed to create stash of Claude changes', vim.log.levels.ERROR)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
-- Inline diff viewer for nvim-claude
|
||||
-- Shows Claude's changes directly in the current buffer with accept/reject functionality
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Namespace for virtual text and highlights
|
||||
local ns_id = vim.api.nvim_create_namespace('nvim_claude_inline_diff')
|
||||
|
||||
-- State tracking
|
||||
M.active_diffs = {} -- Track active inline diffs by buffer number
|
||||
M.original_content = {} -- Store original buffer content
|
||||
|
||||
-- Initialize inline diff for a buffer
|
||||
function M.show_inline_diff(bufnr, old_content, new_content)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Store original content
|
||||
M.original_content[bufnr] = old_content
|
||||
|
||||
-- Get the diff between old and new content
|
||||
local diff_data = M.compute_diff(old_content, new_content)
|
||||
|
||||
if not diff_data or #diff_data.hunks == 0 then
|
||||
vim.notify('No changes to display', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Store diff data for this buffer
|
||||
M.active_diffs[bufnr] = {
|
||||
hunks = diff_data.hunks,
|
||||
new_content = new_content,
|
||||
current_hunk = 1,
|
||||
applied_hunks = {}
|
||||
}
|
||||
|
||||
-- Apply visual indicators
|
||||
M.apply_diff_visualization(bufnr)
|
||||
|
||||
-- Set up buffer-local keymaps
|
||||
M.setup_inline_keymaps(bufnr)
|
||||
|
||||
-- Jump to first hunk
|
||||
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)
|
||||
end
|
||||
|
||||
-- Compute diff between two texts
|
||||
function M.compute_diff(old_text, new_text)
|
||||
local utils = require('nvim-claude.utils')
|
||||
|
||||
-- Write texts to temp files
|
||||
local old_file = '/tmp/nvim-claude-old.txt'
|
||||
local new_file = '/tmp/nvim-claude-new.txt'
|
||||
|
||||
utils.write_file(old_file, old_text)
|
||||
utils.write_file(new_file, new_text)
|
||||
|
||||
-- Generate unified diff
|
||||
local cmd = string.format('diff -u "%s" "%s" || true', old_file, new_file)
|
||||
local diff_output = utils.exec(cmd)
|
||||
|
||||
-- Parse diff into hunks
|
||||
local hunks = M.parse_diff(diff_output)
|
||||
|
||||
return {
|
||||
hunks = hunks
|
||||
}
|
||||
end
|
||||
|
||||
-- Parse unified diff output into hunk structures
|
||||
function M.parse_diff(diff_text)
|
||||
local hunks = {}
|
||||
local current_hunk = nil
|
||||
local in_hunk = false
|
||||
|
||||
for line in diff_text:gmatch('[^\r\n]+') do
|
||||
if line:match('^@@') then
|
||||
-- New hunk header
|
||||
if current_hunk then
|
||||
table.insert(hunks, current_hunk)
|
||||
end
|
||||
|
||||
local old_start, old_count, new_start, new_count = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
|
||||
current_hunk = {
|
||||
old_start = tonumber(old_start),
|
||||
old_count = tonumber(old_count) or 1,
|
||||
new_start = tonumber(new_start),
|
||||
new_count = tonumber(new_count) or 1,
|
||||
lines = {},
|
||||
header = line
|
||||
}
|
||||
in_hunk = true
|
||||
elseif in_hunk and (line:match('^[%+%-]') or line:match('^%s')) then
|
||||
-- Diff line
|
||||
table.insert(current_hunk.lines, line)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add last hunk
|
||||
if current_hunk then
|
||||
table.insert(hunks, current_hunk)
|
||||
end
|
||||
|
||||
return hunks
|
||||
end
|
||||
|
||||
-- Apply visual indicators for diff
|
||||
function M.apply_diff_visualization(bufnr)
|
||||
local diff_data = M.active_diffs[bufnr]
|
||||
if not diff_data then return end
|
||||
|
||||
-- Clear existing highlights
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1)
|
||||
|
||||
-- Apply highlights for each hunk
|
||||
for i, hunk in ipairs(diff_data.hunks) do
|
||||
local line_num = hunk.old_start - 1 -- 0-indexed
|
||||
|
||||
-- Track lines to highlight
|
||||
local del_lines = {}
|
||||
local add_lines = {}
|
||||
local current_line = line_num
|
||||
|
||||
for _, diff_line in ipairs(hunk.lines) do
|
||||
if diff_line:match('^%-') then
|
||||
-- Deletion
|
||||
table.insert(del_lines, current_line)
|
||||
current_line = current_line + 1
|
||||
elseif diff_line:match('^%+') then
|
||||
-- Addition (shown as virtual text)
|
||||
table.insert(add_lines, {
|
||||
line = current_line - 1,
|
||||
text = diff_line:sub(2)
|
||||
})
|
||||
else
|
||||
-- Context line
|
||||
current_line = current_line + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply deletion highlights
|
||||
for _, line in ipairs(del_lines) do
|
||||
vim.api.nvim_buf_add_highlight(bufnr, ns_id, 'DiffDelete', line, 0, -1)
|
||||
end
|
||||
|
||||
-- Add virtual text for additions
|
||||
for _, add in ipairs(add_lines) do
|
||||
vim.api.nvim_buf_set_extmark(bufnr, ns_id, add.line, 0, {
|
||||
virt_lines = {{
|
||||
{' + ' .. add.text, 'DiffAdd'}
|
||||
}},
|
||||
virt_lines_above = false
|
||||
})
|
||||
end
|
||||
|
||||
-- Add hunk header as virtual text
|
||||
vim.api.nvim_buf_set_extmark(bufnr, ns_id, line_num, 0, {
|
||||
virt_lines = {{
|
||||
{hunk.header .. ' [Hunk ' .. i .. '/' .. #diff_data.hunks .. ']', 'Comment'}
|
||||
}},
|
||||
virt_lines_above = true,
|
||||
id = 1000 + i -- Unique ID for hunk headers
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Set up buffer-local keymaps for inline diff
|
||||
function M.setup_inline_keymaps(bufnr)
|
||||
local opts = { buffer = bufnr, silent = true }
|
||||
|
||||
-- Navigation
|
||||
vim.keymap.set('n', ']h', function() M.next_hunk(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Next Claude hunk' }))
|
||||
vim.keymap.set('n', '[h', function() M.prev_hunk(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Previous Claude hunk' }))
|
||||
|
||||
-- Accept/Reject
|
||||
vim.keymap.set('n', '<leader>ia', function() M.accept_current_hunk(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Accept Claude hunk' }))
|
||||
vim.keymap.set('n', '<leader>ir', function() M.reject_current_hunk(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Reject Claude hunk' }))
|
||||
|
||||
-- Accept/Reject all
|
||||
vim.keymap.set('n', '<leader>iA', function() M.accept_all_hunks(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Accept all Claude hunks' }))
|
||||
vim.keymap.set('n', '<leader>iR', function() M.reject_all_hunks(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Reject all Claude hunks' }))
|
||||
|
||||
-- Exit inline diff
|
||||
vim.keymap.set('n', '<leader>iq', function() M.close_inline_diff(bufnr) end,
|
||||
vim.tbl_extend('force', opts, { desc = 'Close inline diff' }))
|
||||
end
|
||||
|
||||
-- Jump to specific hunk
|
||||
function M.jump_to_hunk(bufnr, hunk_idx)
|
||||
local diff_data = M.active_diffs[bufnr]
|
||||
if not diff_data or not diff_data.hunks[hunk_idx] then return end
|
||||
|
||||
local hunk = diff_data.hunks[hunk_idx]
|
||||
diff_data.current_hunk = hunk_idx
|
||||
|
||||
-- Move cursor to hunk start
|
||||
vim.api.nvim_win_set_cursor(0, {hunk.old_start, 0})
|
||||
|
||||
-- Update status
|
||||
vim.notify(string.format('Hunk %d/%d', hunk_idx, #diff_data.hunks), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Navigate to next hunk
|
||||
function M.next_hunk(bufnr)
|
||||
local diff_data = M.active_diffs[bufnr]
|
||||
if not diff_data then return end
|
||||
|
||||
local next_idx = diff_data.current_hunk + 1
|
||||
if next_idx > #diff_data.hunks then
|
||||
next_idx = 1
|
||||
end
|
||||
|
||||
M.jump_to_hunk(bufnr, next_idx)
|
||||
end
|
||||
|
||||
-- Navigate to previous hunk
|
||||
function M.prev_hunk(bufnr)
|
||||
local diff_data = M.active_diffs[bufnr]
|
||||
if not diff_data then return end
|
||||
|
||||
local prev_idx = diff_data.current_hunk - 1
|
||||
if prev_idx < 1 then
|
||||
prev_idx = #diff_data.hunks
|
||||
end
|
||||
|
||||
M.jump_to_hunk(bufnr, prev_idx)
|
||||
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]
|
||||
if not hunk then return end
|
||||
|
||||
-- Apply the hunk changes
|
||||
M.apply_hunk_changes(bufnr, hunk)
|
||||
|
||||
-- Mark as applied
|
||||
diff_data.applied_hunks[diff_data.current_hunk] = true
|
||||
|
||||
-- Refresh visualization
|
||||
M.apply_diff_visualization(bufnr)
|
||||
|
||||
vim.notify(string.format('Accepted hunk %d/%d', diff_data.current_hunk, #diff_data.hunks), vim.log.levels.INFO)
|
||||
|
||||
-- Move to next hunk
|
||||
M.next_hunk(bufnr)
|
||||
end
|
||||
|
||||
-- Reject current hunk
|
||||
function M.reject_current_hunk(bufnr)
|
||||
local diff_data = M.active_diffs[bufnr]
|
||||
if not diff_data then return end
|
||||
|
||||
vim.notify(string.format('Rejected hunk %d/%d', diff_data.current_hunk, #diff_data.hunks), vim.log.levels.INFO)
|
||||
|
||||
-- Move to next hunk
|
||||
M.next_hunk(bufnr)
|
||||
end
|
||||
|
||||
-- Apply hunk changes to buffer
|
||||
function M.apply_hunk_changes(bufnr, hunk)
|
||||
-- Get current buffer lines
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
|
||||
-- Build new lines with hunk applied
|
||||
local new_lines = {}
|
||||
local buffer_line = 1
|
||||
local hunk_line = 1
|
||||
local applied = false
|
||||
|
||||
while buffer_line <= #lines do
|
||||
if buffer_line == hunk.old_start and not applied then
|
||||
-- Apply hunk here
|
||||
for _, diff_line in ipairs(hunk.lines) do
|
||||
if diff_line:match('^%+') then
|
||||
-- Add new line
|
||||
table.insert(new_lines, diff_line:sub(2))
|
||||
elseif diff_line:match('^%-') then
|
||||
-- Skip deleted line
|
||||
buffer_line = buffer_line + 1
|
||||
else
|
||||
-- Keep context line
|
||||
table.insert(new_lines, lines[buffer_line])
|
||||
buffer_line = buffer_line + 1
|
||||
end
|
||||
end
|
||||
applied = true
|
||||
else
|
||||
-- Copy unchanged line
|
||||
if buffer_line <= #lines then
|
||||
table.insert(new_lines, lines[buffer_line])
|
||||
end
|
||||
buffer_line = buffer_line + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Update buffer
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
|
||||
end
|
||||
|
||||
-- Accept all hunks
|
||||
function M.accept_all_hunks(bufnr)
|
||||
local diff_data = M.active_diffs[bufnr]
|
||||
if not diff_data then return end
|
||||
|
||||
-- Replace buffer with new content
|
||||
local new_lines = vim.split(diff_data.new_content, '\n')
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
|
||||
|
||||
vim.notify('Accepted all Claude changes', vim.log.levels.INFO)
|
||||
|
||||
-- Close inline diff
|
||||
M.close_inline_diff(bufnr)
|
||||
end
|
||||
|
||||
-- Reject all hunks
|
||||
function M.reject_all_hunks(bufnr)
|
||||
vim.notify('Rejected all Claude changes', vim.log.levels.INFO)
|
||||
|
||||
-- Close inline diff
|
||||
M.close_inline_diff(bufnr)
|
||||
end
|
||||
|
||||
-- Close inline diff mode
|
||||
function M.close_inline_diff(bufnr)
|
||||
-- Clear highlights and virtual text
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1)
|
||||
|
||||
-- Remove buffer-local keymaps
|
||||
vim.keymap.del('n', ']h', { buffer = bufnr })
|
||||
vim.keymap.del('n', '[h', { buffer = bufnr })
|
||||
vim.keymap.del('n', '<leader>ia', { buffer = bufnr })
|
||||
vim.keymap.del('n', '<leader>ir', { buffer = bufnr })
|
||||
vim.keymap.del('n', '<leader>iA', { buffer = bufnr })
|
||||
vim.keymap.del('n', '<leader>iR', { buffer = bufnr })
|
||||
vim.keymap.del('n', '<leader>iq', { buffer = bufnr })
|
||||
|
||||
-- Clean up state
|
||||
M.active_diffs[bufnr] = nil
|
||||
M.original_content[bufnr] = nil
|
||||
|
||||
vim.notify('Inline diff closed', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Check if buffer has active inline diff
|
||||
function M.has_active_diff(bufnr)
|
||||
return M.active_diffs[bufnr] ~= nil
|
||||
end
|
||||
|
||||
return M
|
Loading…
Reference in New Issue