remove nvim-claude to convert to submodule
parent
6f18060a51
commit
e26997b7fa
File diff suppressed because it is too large
Load Diff
@ -1,853 +0,0 @@
|
||||
-- Diff review system for nvim-claude using diffview.nvim
|
||||
local M = {}
|
||||
|
||||
-- State tracking - make it persistent across hook calls
|
||||
M.current_review = M.current_review or nil
|
||||
|
||||
function M.setup()
|
||||
-- Set up keybindings
|
||||
M.setup_keybindings()
|
||||
|
||||
vim.notify('Diff review system loaded (using diffview.nvim)', vim.log.levels.DEBUG)
|
||||
end
|
||||
|
||||
-- Handle Claude edit completion
|
||||
function M.handle_claude_edit(stash_ref, pre_edit_ref)
|
||||
if not stash_ref then
|
||||
vim.notify('No stash reference provided for diff review', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.notify('Processing Claude edit with stash: ' .. stash_ref, vim.log.levels.INFO)
|
||||
|
||||
-- Get list of changed files
|
||||
local changed_files = M.get_changed_files(stash_ref)
|
||||
if not changed_files or #changed_files == 0 then
|
||||
vim.notify('No changes detected from Claude edit', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Initialize review session
|
||||
M.current_review = {
|
||||
stash_ref = stash_ref,
|
||||
pre_edit_ref = pre_edit_ref, -- Store the pre-edit commit reference
|
||||
timestamp = os.time(),
|
||||
changed_files = changed_files,
|
||||
}
|
||||
|
||||
-- Notify user about changes
|
||||
vim.notify(string.format('Claude made changes to %d file(s): %s', #changed_files, table.concat(changed_files, ', ')), vim.log.levels.INFO)
|
||||
|
||||
vim.notify('Use <leader>dd to open diffview, <leader>df for fugitive, <leader>dc to clear review', vim.log.levels.INFO)
|
||||
|
||||
-- Automatically open diffview
|
||||
M.open_diffview()
|
||||
end
|
||||
|
||||
-- Handle Claude stashes (only show Claude changes)
|
||||
function M.handle_claude_stashes(baseline_ref)
|
||||
if not baseline_ref then
|
||||
vim.notify('No baseline reference provided for Claude stashes', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.notify('Showing Claude stashes against baseline: ' .. baseline_ref, vim.log.levels.INFO)
|
||||
|
||||
-- Get Claude stashes
|
||||
local claude_stashes = M.get_claude_stashes()
|
||||
if not claude_stashes or #claude_stashes == 0 then
|
||||
vim.notify('No Claude stashes found', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Initialize review session for Claude stashes
|
||||
M.current_review = {
|
||||
baseline_ref = baseline_ref,
|
||||
timestamp = os.time(),
|
||||
claude_stashes = claude_stashes,
|
||||
current_stash_index = 0, -- Show cumulative view by default
|
||||
is_stash_based = true,
|
||||
}
|
||||
|
||||
-- Notify user about changes
|
||||
vim.notify(string.format('Found %d Claude stash(es). Use <leader>dd for cumulative view, <leader>dh to browse.', #claude_stashes), vim.log.levels.INFO)
|
||||
|
||||
-- Automatically open cumulative stash view
|
||||
M.open_cumulative_stash_view()
|
||||
end
|
||||
|
||||
-- Handle cumulative diff (always show against baseline) - legacy support
|
||||
function M.handle_cumulative_diff(baseline_ref)
|
||||
-- Redirect to new stash-based handler
|
||||
M.handle_claude_stashes(baseline_ref)
|
||||
end
|
||||
|
||||
-- Get list of files changed in the stash
|
||||
function M.get_changed_files(stash_ref)
|
||||
local utils = require 'nvim-claude.utils'
|
||||
local cmd = string.format('git stash show %s --name-only', stash_ref)
|
||||
local result = utils.exec(cmd)
|
||||
|
||||
if not result or result == '' then
|
||||
return {}
|
||||
end
|
||||
|
||||
local files = {}
|
||||
for line in result:gmatch '[^\n]+' do
|
||||
if line ~= '' then
|
||||
table.insert(files, line)
|
||||
end
|
||||
end
|
||||
return files
|
||||
end
|
||||
|
||||
-- Get list of files changed since baseline
|
||||
function M.get_changed_files_since_baseline(baseline_ref)
|
||||
local utils = require 'nvim-claude.utils'
|
||||
local cmd = string.format('git diff --name-only %s', baseline_ref)
|
||||
local result = utils.exec(cmd)
|
||||
|
||||
if not result or result == '' then
|
||||
return {}
|
||||
end
|
||||
|
||||
local files = {}
|
||||
for line in result:gmatch '[^\n]+' do
|
||||
if line ~= '' then
|
||||
table.insert(files, line)
|
||||
end
|
||||
end
|
||||
return files
|
||||
end
|
||||
|
||||
-- Get Claude stashes (only stashes with [claude-edit] messages)
|
||||
function M.get_claude_stashes()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
local cmd = 'git stash list'
|
||||
local result = utils.exec(cmd)
|
||||
|
||||
if not result or result == '' then
|
||||
return {}
|
||||
end
|
||||
|
||||
local stashes = {}
|
||||
for line in result:gmatch '[^\n]+' do
|
||||
if line ~= '' and line:match '%[claude%-edit%]' then
|
||||
local stash_ref = line:match '^(stash@{%d+})'
|
||||
if stash_ref then
|
||||
table.insert(stashes, {
|
||||
ref = stash_ref,
|
||||
message = line:match ': (.+)$' or line,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
return stashes
|
||||
end
|
||||
|
||||
-- Set up keybindings for diff review
|
||||
function M.setup_keybindings()
|
||||
-- Review actions
|
||||
vim.keymap.set('n', '<leader>dd', M.open_diffview, { desc = 'Open Claude diff in diffview' })
|
||||
vim.keymap.set('n', '<leader>df', M.open_fugitive, { desc = 'Open Claude diff in fugitive' })
|
||||
vim.keymap.set('n', '<leader>dc', M.clear_review, { desc = 'Clear Claude review session' })
|
||||
vim.keymap.set('n', '<leader>dl', M.list_changes, { desc = 'List Claude changed files' })
|
||||
vim.keymap.set('n', '<leader>da', M.accept_changes, { desc = 'Accept all Claude changes' })
|
||||
vim.keymap.set('n', '<leader>dr', M.decline_changes, { desc = 'Decline all Claude changes' })
|
||||
|
||||
-- Stash browsing
|
||||
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
|
||||
function M.open_diffview()
|
||||
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, -- Show cumulative view by default
|
||||
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
|
||||
|
||||
-- Use stash-based diff if available
|
||||
if M.current_review.is_stash_based then
|
||||
M.open_cumulative_stash_view()
|
||||
return
|
||||
end
|
||||
|
||||
-- Legacy: Use cumulative diff if available
|
||||
if M.current_review.is_cumulative then
|
||||
M.open_cumulative_diffview()
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if diffview is available
|
||||
local ok, diffview = pcall(require, 'diffview')
|
||||
if not ok then
|
||||
vim.notify('diffview.nvim not available, falling back to fugitive', vim.log.levels.WARN)
|
||||
M.open_fugitive()
|
||||
return
|
||||
end
|
||||
|
||||
-- Use the pre-edit reference if available
|
||||
if M.current_review.pre_edit_ref then
|
||||
local cmd = 'DiffviewOpen ' .. M.current_review.pre_edit_ref
|
||||
vim.notify('Opening diffview with pre-edit commit: ' .. cmd, vim.log.levels.INFO)
|
||||
vim.cmd(cmd)
|
||||
else
|
||||
-- Fallback to comparing stash with its parent
|
||||
vim.notify('No pre-edit commit found, falling back to stash comparison', vim.log.levels.WARN)
|
||||
local cmd = string.format('DiffviewOpen %s^..%s', M.current_review.stash_ref, M.current_review.stash_ref)
|
||||
vim.notify('Opening diffview: ' .. cmd, vim.log.levels.INFO)
|
||||
vim.cmd(cmd)
|
||||
end
|
||||
end
|
||||
|
||||
-- Open cumulative stash view (shows all Claude changes since baseline)
|
||||
function M.open_cumulative_stash_view()
|
||||
if not M.current_review then
|
||||
vim.notify('No active review session', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if diffview is available
|
||||
local ok, diffview = pcall(require, 'diffview')
|
||||
if not ok then
|
||||
vim.notify('diffview.nvim not available', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
if M.current_review.is_stash_based and M.current_review.claude_stashes then
|
||||
-- Show cumulative diff of all Claude stashes against baseline
|
||||
local cmd = 'DiffviewOpen ' .. M.current_review.baseline_ref
|
||||
vim.notify('Opening cumulative Claude stash view: ' .. cmd, vim.log.levels.INFO)
|
||||
vim.cmd(cmd)
|
||||
else
|
||||
-- Fallback to old behavior
|
||||
local cmd = 'DiffviewOpen ' .. M.current_review.baseline_ref
|
||||
vim.notify('Opening cumulative diffview: ' .. cmd, vim.log.levels.INFO)
|
||||
vim.cmd(cmd)
|
||||
end
|
||||
end
|
||||
|
||||
-- Open cumulative diffview (always against baseline) - legacy support
|
||||
function M.open_cumulative_diffview()
|
||||
M.open_cumulative_stash_view()
|
||||
end
|
||||
|
||||
-- Open fugitive diff (fallback)
|
||||
function M.open_fugitive()
|
||||
if not M.current_review then
|
||||
vim.notify('No active review session', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Use fugitive to show diff
|
||||
local cmd = 'Gdiffsplit ' .. M.current_review.stash_ref
|
||||
vim.notify('Opening fugitive: ' .. cmd, vim.log.levels.INFO)
|
||||
vim.cmd(cmd)
|
||||
end
|
||||
|
||||
-- List changed files
|
||||
function M.list_changes()
|
||||
if not M.current_review then
|
||||
vim.notify('No active review session', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local files = M.current_review.changed_files
|
||||
if #files == 0 then
|
||||
vim.notify('No changes found', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create a telescope picker if available, otherwise just notify
|
||||
local ok, telescope = pcall(require, 'telescope.pickers')
|
||||
if ok then
|
||||
M.telescope_changed_files()
|
||||
else
|
||||
vim.notify('Changed files:', vim.log.levels.INFO)
|
||||
for i, file in ipairs(files) do
|
||||
vim.notify(string.format(' %d. %s', i, file), vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Telescope picker for changed files
|
||||
function M.telescope_changed_files()
|
||||
local pickers = require 'telescope.pickers'
|
||||
local finders = require 'telescope.finders'
|
||||
local conf = require('telescope.config').values
|
||||
|
||||
pickers
|
||||
.new({}, {
|
||||
prompt_title = 'Claude Changed Files',
|
||||
finder = finders.new_table {
|
||||
results = M.current_review.changed_files,
|
||||
},
|
||||
sorter = conf.generic_sorter {},
|
||||
attach_mappings = function(_, map)
|
||||
map('i', '<CR>', function(prompt_bufnr)
|
||||
local selection = require('telescope.actions.state').get_selected_entry()
|
||||
require('telescope.actions').close(prompt_bufnr)
|
||||
vim.cmd('edit ' .. selection[1])
|
||||
M.open_diffview()
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
:find()
|
||||
end
|
||||
|
||||
-- Clear review session
|
||||
function M.clear_review()
|
||||
if M.current_review then
|
||||
M.current_review = nil
|
||||
|
||||
-- Close diffview if it's open
|
||||
pcall(function()
|
||||
vim.cmd 'DiffviewClose'
|
||||
end)
|
||||
|
||||
vim.notify('Claude review session cleared', vim.log.levels.INFO)
|
||||
else
|
||||
vim.notify('No active Claude review session', vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
-- Accept all Claude changes (update baseline)
|
||||
function M.accept_changes()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
|
||||
-- Get project root
|
||||
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
|
||||
|
||||
-- Create new baseline commit with current state
|
||||
local timestamp = os.time()
|
||||
local commit_msg = string.format('claude-baseline-%d', timestamp)
|
||||
|
||||
-- Stage all changes
|
||||
local add_cmd = string.format('cd "%s" && git add -A', git_root)
|
||||
local add_result, add_err = utils.exec(add_cmd)
|
||||
|
||||
if add_err then
|
||||
vim.notify('Failed to stage changes: ' .. add_err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create new baseline commit
|
||||
local commit_cmd = string.format('cd "%s" && git commit -m "%s" --allow-empty', git_root, commit_msg)
|
||||
local commit_result, commit_err = utils.exec(commit_cmd)
|
||||
|
||||
if commit_err and not commit_err:match 'nothing to commit' then
|
||||
vim.notify('Failed to create new baseline: ' .. commit_err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Update baseline reference
|
||||
local baseline_file = '/tmp/claude-baseline-commit'
|
||||
utils.write_file(baseline_file, 'HEAD')
|
||||
|
||||
-- Clear review session
|
||||
M.current_review = nil
|
||||
|
||||
-- Close diffview
|
||||
pcall(function()
|
||||
vim.cmd 'DiffviewClose'
|
||||
end)
|
||||
|
||||
vim.notify('All Claude changes accepted! New baseline created.', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Decline all Claude changes (reset to baseline)
|
||||
function M.decline_changes()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
|
||||
-- Get baseline commit
|
||||
local baseline_file = '/tmp/claude-baseline-commit'
|
||||
local baseline_ref = utils.read_file(baseline_file)
|
||||
|
||||
if not baseline_ref or baseline_ref == '' then
|
||||
vim.notify('No baseline commit found', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
baseline_ref = baseline_ref:gsub('%s+', '')
|
||||
|
||||
-- Get project root
|
||||
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
|
||||
|
||||
-- Reset to baseline (hard reset)
|
||||
local reset_cmd = string.format('cd "%s" && git reset --hard %s', git_root, baseline_ref)
|
||||
local reset_result, reset_err = utils.exec(reset_cmd)
|
||||
|
||||
if reset_err then
|
||||
vim.notify('Failed to reset to baseline: ' .. reset_err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Clear review session
|
||||
M.current_review = nil
|
||||
|
||||
-- Close diffview
|
||||
pcall(function()
|
||||
vim.cmd 'DiffviewClose'
|
||||
end)
|
||||
|
||||
-- Refresh buffers
|
||||
vim.cmd 'checktime'
|
||||
|
||||
vim.notify('All Claude changes declined! Reset to baseline.', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Browse Claude stashes (show list)
|
||||
function M.browse_claude_stashes()
|
||||
if not M.current_review or not M.current_review.is_stash_based then
|
||||
vim.notify('No Claude stash session active', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local stashes = M.current_review.claude_stashes
|
||||
if not stashes or #stashes == 0 then
|
||||
vim.notify('No Claude stashes found', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create a telescope picker if available, otherwise just notify
|
||||
local ok, telescope = pcall(require, 'telescope.pickers')
|
||||
if ok then
|
||||
M.telescope_claude_stashes()
|
||||
else
|
||||
vim.notify('Claude stashes:', vim.log.levels.INFO)
|
||||
for i, stash in ipairs(stashes) do
|
||||
local marker = (i == M.current_review.current_stash_index) and ' [current]' or ''
|
||||
vim.notify(string.format(' %d. %s%s', i, stash.message, marker), vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- View previous Claude stash
|
||||
function M.previous_stash()
|
||||
if not M.current_review or not M.current_review.is_stash_based then
|
||||
vim.notify('No Claude stash session active', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local stashes = M.current_review.claude_stashes
|
||||
if not stashes or #stashes == 0 then
|
||||
vim.notify('No Claude stashes found', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local current_index = M.current_review.current_stash_index or 0
|
||||
if current_index <= 1 then
|
||||
vim.notify('Already at first stash', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
M.current_review.current_stash_index = current_index - 1
|
||||
M.view_specific_stash(M.current_review.current_stash_index)
|
||||
end
|
||||
|
||||
-- View next Claude stash
|
||||
function M.next_stash()
|
||||
if not M.current_review or not M.current_review.is_stash_based then
|
||||
vim.notify('No Claude stash session active', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local stashes = M.current_review.claude_stashes
|
||||
if not stashes or #stashes == 0 then
|
||||
vim.notify('No Claude stashes found', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local current_index = M.current_review.current_stash_index or 0
|
||||
if current_index >= #stashes then
|
||||
vim.notify('Already at last stash', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
M.current_review.current_stash_index = current_index + 1
|
||||
M.view_specific_stash(M.current_review.current_stash_index)
|
||||
end
|
||||
|
||||
-- View a specific stash by index
|
||||
function M.view_specific_stash(index)
|
||||
if not M.current_review or not M.current_review.is_stash_based then
|
||||
vim.notify('No Claude stash session active', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local stashes = M.current_review.claude_stashes
|
||||
if not stashes or index < 1 or index > #stashes then
|
||||
vim.notify('Invalid stash index', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local stash = stashes[index]
|
||||
|
||||
-- Check if diffview is available
|
||||
local ok, diffview = pcall(require, 'diffview')
|
||||
if not ok then
|
||||
vim.notify('diffview.nvim not available', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Open diffview for this specific stash
|
||||
local cmd = string.format('DiffviewOpen %s^..%s', stash.ref, stash.ref)
|
||||
vim.notify(string.format('Opening stash %d: %s', index, stash.message), vim.log.levels.INFO)
|
||||
vim.cmd(cmd)
|
||||
end
|
||||
|
||||
-- Telescope picker for Claude stashes
|
||||
function M.telescope_claude_stashes()
|
||||
local pickers = require 'telescope.pickers'
|
||||
local finders = require 'telescope.finders'
|
||||
local conf = require('telescope.config').values
|
||||
|
||||
local stashes = M.current_review.claude_stashes
|
||||
local stash_entries = {}
|
||||
|
||||
for i, stash in ipairs(stashes) do
|
||||
table.insert(stash_entries, {
|
||||
value = i,
|
||||
display = string.format('%d. %s', i, stash.message),
|
||||
ordinal = stash.message,
|
||||
})
|
||||
end
|
||||
|
||||
pickers
|
||||
.new({}, {
|
||||
prompt_title = 'Claude Stash History',
|
||||
finder = finders.new_table {
|
||||
results = stash_entries,
|
||||
entry_maker = function(entry)
|
||||
return {
|
||||
value = entry.value,
|
||||
display = entry.display,
|
||||
ordinal = entry.ordinal,
|
||||
}
|
||||
end,
|
||||
},
|
||||
sorter = conf.generic_sorter {},
|
||||
attach_mappings = function(_, map)
|
||||
map('i', '<CR>', function(prompt_bufnr)
|
||||
local selection = require('telescope.actions.state').get_selected_entry()
|
||||
require('telescope.actions').close(prompt_bufnr)
|
||||
M.current_review.current_stash_index = selection.value
|
||||
M.view_specific_stash(selection.value)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
: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
|
||||
|
@ -1,186 +0,0 @@
|
||||
-- Git operations module for nvim-claude
|
||||
local M = {}
|
||||
local utils = require('nvim-claude.utils')
|
||||
|
||||
M.config = {}
|
||||
|
||||
function M.setup(config)
|
||||
M.config = config or {}
|
||||
end
|
||||
|
||||
-- Check if git worktrees are supported
|
||||
function M.supports_worktrees()
|
||||
local result = utils.exec('git worktree list 2>/dev/null')
|
||||
return result ~= nil and not result:match('error')
|
||||
end
|
||||
|
||||
-- Get list of existing worktrees
|
||||
function M.list_worktrees()
|
||||
local result = utils.exec('git worktree list --porcelain')
|
||||
if not result then return {} end
|
||||
|
||||
local worktrees = {}
|
||||
local current = {}
|
||||
|
||||
for line in result:gmatch('[^\n]+') do
|
||||
if line:match('^worktree ') then
|
||||
if current.path then
|
||||
table.insert(worktrees, current)
|
||||
end
|
||||
current = { path = line:match('^worktree (.+)') }
|
||||
elseif line:match('^HEAD ') then
|
||||
current.head = line:match('^HEAD (.+)')
|
||||
elseif line:match('^branch ') then
|
||||
current.branch = line:match('^branch (.+)')
|
||||
end
|
||||
end
|
||||
|
||||
if current.path then
|
||||
table.insert(worktrees, current)
|
||||
end
|
||||
|
||||
return worktrees
|
||||
end
|
||||
|
||||
-- Create a new worktree
|
||||
function M.create_worktree(path, branch)
|
||||
branch = branch or M.default_branch()
|
||||
|
||||
-- Check if worktree already exists
|
||||
local worktrees = M.list_worktrees()
|
||||
for _, wt in ipairs(worktrees) do
|
||||
if wt.path == path then
|
||||
return true, wt
|
||||
end
|
||||
end
|
||||
|
||||
-- Generate unique branch name for the worktree
|
||||
local worktree_branch = 'agent-' .. utils.timestamp()
|
||||
|
||||
-- Create worktree with new branch based on specified branch
|
||||
local cmd = string.format('git worktree add -b "%s" "%s" "%s" 2>&1', worktree_branch, path, branch)
|
||||
local result, err = utils.exec(cmd)
|
||||
|
||||
if err then
|
||||
return false, result
|
||||
end
|
||||
|
||||
-- For background agents, ensure no hooks by creating empty .claude directory
|
||||
-- This prevents inline diffs from triggering
|
||||
local claude_dir = path .. '/.claude'
|
||||
if vim.fn.isdirectory(claude_dir) == 0 then
|
||||
vim.fn.mkdir(claude_dir, 'p')
|
||||
end
|
||||
|
||||
-- Create empty settings.json to disable hooks
|
||||
local empty_settings = '{"hooks": {}}'
|
||||
utils.write_file(claude_dir .. '/settings.json', empty_settings)
|
||||
|
||||
return true, { path = path, branch = worktree_branch, base_branch = branch }
|
||||
end
|
||||
|
||||
-- Remove a worktree
|
||||
function M.remove_worktree(path)
|
||||
-- First try to remove as git worktree
|
||||
local cmd = string.format('git worktree remove "%s" --force 2>&1', path)
|
||||
local result, err = utils.exec(cmd)
|
||||
|
||||
-- If it's not a worktree or already removed, just delete the directory
|
||||
if err or result:match('not a working tree') then
|
||||
local rm_cmd = string.format('rm -rf "%s"', path)
|
||||
utils.exec(rm_cmd)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Add entry to .gitignore
|
||||
function M.add_to_gitignore(pattern)
|
||||
local gitignore_path = utils.get_project_root() .. '/.gitignore'
|
||||
|
||||
-- Read existing content
|
||||
local content = utils.read_file(gitignore_path) or ''
|
||||
|
||||
-- Check if pattern already exists
|
||||
if content:match('\n' .. pattern:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') .. '\n') or
|
||||
content:match('^' .. pattern:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') .. '\n') then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Append pattern
|
||||
if not content:match('\n$') and content ~= '' then
|
||||
content = content .. '\n'
|
||||
end
|
||||
content = content .. pattern .. '\n'
|
||||
|
||||
return utils.write_file(gitignore_path, content)
|
||||
end
|
||||
|
||||
-- Get current branch
|
||||
function M.current_branch()
|
||||
local result = utils.exec('git branch --show-current 2>/dev/null')
|
||||
if result then
|
||||
return result:gsub('\n', '')
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get default branch (usually main or master)
|
||||
function M.default_branch()
|
||||
-- Try to get the default branch from remote
|
||||
local result = utils.exec('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null')
|
||||
if result and result ~= '' then
|
||||
local branch = result:match('refs/remotes/origin/(.+)')
|
||||
if branch then
|
||||
return branch:gsub('\n', '')
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: check if main or master exists
|
||||
local main_exists = utils.exec('git show-ref --verify --quiet refs/heads/main')
|
||||
if main_exists and main_exists == '' then
|
||||
return 'main'
|
||||
end
|
||||
|
||||
local master_exists = utils.exec('git show-ref --verify --quiet refs/heads/master')
|
||||
if master_exists and master_exists == '' then
|
||||
return 'master'
|
||||
end
|
||||
|
||||
-- Final fallback
|
||||
return 'main'
|
||||
end
|
||||
|
||||
-- Get git status
|
||||
function M.status(path)
|
||||
local cmd = 'git status --porcelain'
|
||||
if path then
|
||||
cmd = string.format('cd "%s" && %s', path, cmd)
|
||||
end
|
||||
|
||||
local result = utils.exec(cmd)
|
||||
if not result then return {} end
|
||||
|
||||
local files = {}
|
||||
for line in result:gmatch('[^\n]+') do
|
||||
local status, file = line:match('^(..) (.+)$')
|
||||
if status and file then
|
||||
table.insert(files, { status = status, file = file })
|
||||
end
|
||||
end
|
||||
|
||||
return files
|
||||
end
|
||||
|
||||
-- Get diff between two paths
|
||||
function M.diff(path1, path2)
|
||||
local cmd = string.format(
|
||||
'git diff --no-index --name-status "%s" "%s" 2>/dev/null',
|
||||
path1,
|
||||
path2
|
||||
)
|
||||
local result = utils.exec(cmd)
|
||||
return result or ''
|
||||
end
|
||||
|
||||
return M
|
@ -1,726 +0,0 @@
|
||||
-- Claude Code hooks integration for nvim-claude
|
||||
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
|
||||
|
||||
-- Save the updated state
|
||||
persistence.save_state({ stash_ref = stash_hash })
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
-- Setup persistence layer on startup
|
||||
vim.defer_fn(function()
|
||||
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
|
||||
function M.pre_tool_use_hook()
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
|
||||
-- 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
|
||||
|
||||
-- Post-tool-use hook: Create stash of Claude's changes and trigger diff review
|
||||
function M.post_tool_use_hook()
|
||||
-- Run directly without vim.schedule for testing
|
||||
local utils = require 'nvim-claude.utils'
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
|
||||
-- Refresh all buffers to show Claude's changes
|
||||
vim.cmd 'checktime'
|
||||
|
||||
-- Check if Claude made any changes
|
||||
local git_root = utils.get_project_root()
|
||||
if not git_root then
|
||||
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
|
||||
return
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- Always use the stable baseline reference for comparison
|
||||
local stash_ref = M.stable_baseline_ref or persistence.current_stash_ref
|
||||
|
||||
-- If no baseline exists at all, create one now (shouldn't happen normally)
|
||||
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 then
|
||||
-- Process inline diffs for currently open buffers
|
||||
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
|
||||
-- Show inline diff for this open buffer
|
||||
M.show_inline_diff_for_file(buf, file, git_root, stash_ref)
|
||||
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
|
||||
|
||||
if opened_inline then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- If no inline diff was shown, just notify the user
|
||||
if not opened_inline then
|
||||
vim.notify('Claude made changes. Open the modified files to see inline diffs.', vim.log.levels.INFO)
|
||||
end
|
||||
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)
|
||||
|
||||
local utils = require 'nvim-claude.utils'
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
local git_root = utils.get_project_root()
|
||||
|
||||
if not git_root then
|
||||
vim.notify('Not in git repository', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get current buffer
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local buf_name = vim.api.nvim_buf_get_name(bufnr)
|
||||
|
||||
if buf_name == '' then
|
||||
vim.notify('Current buffer has no file', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get relative path
|
||||
local relative_path = buf_name:gsub(git_root .. '/', '')
|
||||
vim.notify('Testing inline diff for: ' .. relative_path, vim.log.levels.INFO)
|
||||
|
||||
-- Get baseline content - check for updated baseline first
|
||||
local inline_diff = require 'nvim-claude.inline-diff'
|
||||
local original_content = nil
|
||||
|
||||
-- Check if we have an updated baseline in memory
|
||||
vim.notify('DEBUG: Checking for baseline in buffer ' .. bufnr, vim.log.levels.INFO)
|
||||
vim.notify('DEBUG: Available baselines: ' .. vim.inspect(vim.tbl_keys(inline_diff.original_content)), vim.log.levels.INFO)
|
||||
|
||||
if inline_diff.original_content[bufnr] then
|
||||
original_content = inline_diff.original_content[bufnr]
|
||||
vim.notify('Using updated baseline from memory (length: ' .. #original_content .. ')', vim.log.levels.INFO)
|
||||
elseif persistence.current_stash_ref then
|
||||
-- Try to get from stash
|
||||
local stash_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, persistence.current_stash_ref, relative_path)
|
||||
local git_err
|
||||
original_content, git_err = utils.exec(stash_cmd)
|
||||
|
||||
if git_err then
|
||||
vim.notify('Failed to get stash content: ' .. git_err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.notify('Using stash baseline: ' .. persistence.current_stash_ref, vim.log.levels.INFO)
|
||||
else
|
||||
-- Fall back to HEAD
|
||||
local baseline_cmd = string.format('cd "%s" && git show HEAD:%s 2>/dev/null', git_root, relative_path)
|
||||
local git_err
|
||||
original_content, git_err = utils.exec(baseline_cmd)
|
||||
|
||||
if git_err then
|
||||
vim.notify('Failed to get baseline content: ' .. git_err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.notify('Using HEAD as baseline', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Get current content
|
||||
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local current_content = table.concat(current_lines, '\n')
|
||||
|
||||
-- Show inline diff
|
||||
inline_diff.show_inline_diff(bufnr, original_content, current_content)
|
||||
end
|
||||
|
||||
-- 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
|
||||
persistence.setup_autocmds()
|
||||
|
||||
-- Try to restore any saved diffs
|
||||
local restored = persistence.restore_diffs()
|
||||
|
||||
-- Also restore the baseline reference from persistence if it exists
|
||||
if persistence.current_stash_ref then
|
||||
M.stable_baseline_ref = persistence.current_stash_ref
|
||||
vim.notify('Restored baseline: ' .. M.stable_baseline_ref, vim.log.levels.DEBUG)
|
||||
end
|
||||
|
||||
-- Don't create a startup baseline - only create baselines when Claude makes edits
|
||||
end
|
||||
|
||||
-- Manual hook testing
|
||||
function M.test_hooks()
|
||||
vim.notify('=== Testing nvim-claude hooks ===', vim.log.levels.INFO)
|
||||
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
|
||||
-- Test creating a stash
|
||||
vim.notify('1. Creating test stash...', vim.log.levels.INFO)
|
||||
local stash_ref = persistence.create_stash('nvim-claude: test stash')
|
||||
|
||||
if stash_ref then
|
||||
persistence.current_stash_ref = stash_ref
|
||||
vim.notify('Stash created: ' .. stash_ref, vim.log.levels.INFO)
|
||||
else
|
||||
vim.notify('Failed to create stash', vim.log.levels.ERROR)
|
||||
end
|
||||
|
||||
-- Simulate making a change
|
||||
vim.notify('2. Make some changes to test files now...', vim.log.levels.INFO)
|
||||
|
||||
-- Test post-tool-use hook after a delay
|
||||
vim.notify('3. Will trigger post-tool-use hook in 3 seconds...', vim.log.levels.INFO)
|
||||
|
||||
vim.defer_fn(function()
|
||||
M.post_tool_use_hook()
|
||||
end, 3000)
|
||||
|
||||
vim.notify('=== Hook testing started - make changes now! ===', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
-- Install Claude Code hooks
|
||||
function M.install_hooks()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
|
||||
-- Get project root
|
||||
local project_root = utils.get_project_root()
|
||||
if not project_root then
|
||||
vim.notify('Not in a git repository', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create .claude directory
|
||||
local claude_dir = project_root .. '/.claude'
|
||||
if not vim.fn.isdirectory(claude_dir) then
|
||||
vim.fn.mkdir(claude_dir, 'p')
|
||||
end
|
||||
|
||||
-- Create hooks configuration
|
||||
local server_name = vim.v.servername or 'NVIM'
|
||||
local pre_command = string.format(
|
||||
'nvim --headless --server %s --remote-send "<C-\\><C-N>:lua require(\'nvim-claude.hooks\').pre_tool_use_hook()<CR>" 2>/dev/null || true',
|
||||
server_name
|
||||
)
|
||||
local post_command = string.format(
|
||||
'nvim --headless --server %s --remote-send "<C-\\><C-N>:lua require(\'nvim-claude.hooks\').post_tool_use_hook()<CR>" 2>/dev/null || true',
|
||||
server_name
|
||||
)
|
||||
|
||||
local hooks_config = {
|
||||
hooks = {
|
||||
PreToolUse = {
|
||||
{
|
||||
matcher = 'Edit|Write|MultiEdit', -- Only match file editing tools
|
||||
hooks = {
|
||||
{
|
||||
type = 'command',
|
||||
command = pre_command,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PostToolUse = {
|
||||
{
|
||||
matcher = 'Edit|Write|MultiEdit', -- Only match file editing tools
|
||||
hooks = {
|
||||
{
|
||||
type = 'command',
|
||||
command = post_command,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- Write hooks configuration
|
||||
local settings_file = claude_dir .. '/settings.json'
|
||||
local success, err = utils.write_json(settings_file, hooks_config)
|
||||
|
||||
if success then
|
||||
-- Add .claude to gitignore if needed
|
||||
local gitignore_path = project_root .. '/.gitignore'
|
||||
local gitignore_content = utils.read_file(gitignore_path) or ''
|
||||
|
||||
if not gitignore_content:match '%.claude/' then
|
||||
local new_content = gitignore_content .. '\n# Claude Code hooks\n.claude/\n'
|
||||
utils.write_file(gitignore_path, new_content)
|
||||
vim.notify('Added .claude/ to .gitignore', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
vim.notify('Claude Code hooks installed successfully', vim.log.levels.INFO)
|
||||
vim.notify('Hooks configuration written to: ' .. settings_file, vim.log.levels.INFO)
|
||||
else
|
||||
vim.notify('Failed to install hooks: ' .. (err or 'unknown error'), vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
-- Uninstall Claude Code hooks
|
||||
function M.uninstall_hooks()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
|
||||
-- Get project root
|
||||
local project_root = utils.get_project_root()
|
||||
if not project_root then
|
||||
vim.notify('Not in a git repository', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local settings_file = project_root .. '/.claude/settings.json'
|
||||
|
||||
if vim.fn.filereadable(settings_file) then
|
||||
vim.fn.delete(settings_file)
|
||||
vim.notify('Claude Code hooks uninstalled', vim.log.levels.INFO)
|
||||
else
|
||||
vim.notify('No hooks configuration found', vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
-- Commands for manual hook management
|
||||
function M.setup_commands()
|
||||
vim.api.nvim_create_user_command('ClaudeTestHooks', function()
|
||||
M.test_hooks()
|
||||
end, {
|
||||
desc = 'Test Claude Code hooks',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeTestInlineDiff', function()
|
||||
M.test_inline_diff()
|
||||
end, {
|
||||
desc = 'Test Claude inline diff manually',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeTestKeymap', function()
|
||||
require('nvim-claude.inline-diff').test_keymap()
|
||||
end, {
|
||||
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()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local current_content = table.concat(current_lines, '\n')
|
||||
|
||||
local inline_diff = require 'nvim-claude.inline-diff'
|
||||
inline_diff.original_content[bufnr] = current_content
|
||||
|
||||
-- Save updated state
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
if persistence.current_stash_ref then
|
||||
persistence.save_state({ stash_ref = persistence.current_stash_ref })
|
||||
end
|
||||
|
||||
vim.notify('Baseline updated to current buffer state', vim.log.levels.INFO)
|
||||
end, {
|
||||
desc = 'Update Claude baseline to current buffer state',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeTestDiff', function()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
|
||||
-- Check if we're in a git repository
|
||||
local git_root = utils.get_project_root()
|
||||
if not git_root then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if there are any changes
|
||||
local status_cmd = string.format('cd "%s" && git status --porcelain', git_root)
|
||||
local status_result = utils.exec(status_cmd)
|
||||
|
||||
if not status_result or status_result == '' then
|
||||
vim.notify('No changes to test', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create test stash without restoring (to avoid conflicts)
|
||||
local timestamp = os.date '%Y-%m-%d %H:%M:%S'
|
||||
local stash_msg = string.format('[claude-test] %s', timestamp)
|
||||
|
||||
local stash_cmd = string.format('cd "%s" && git stash push -u -m "%s"', git_root, stash_msg)
|
||||
local stash_result, stash_err = utils.exec(stash_cmd)
|
||||
|
||||
if stash_err then
|
||||
vim.notify('Failed to create test stash: ' .. stash_err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Trigger diff review with the stash (no pre-edit ref for manual test)
|
||||
local diff_review = require 'nvim-claude.diff-review'
|
||||
diff_review.handle_claude_edit('stash@{0}', nil)
|
||||
|
||||
-- Pop the stash to restore changes
|
||||
vim.defer_fn(function()
|
||||
local pop_cmd = string.format('cd "%s" && git stash pop --quiet', git_root)
|
||||
utils.exec(pop_cmd)
|
||||
vim.cmd 'checktime' -- Refresh buffers
|
||||
end, 100)
|
||||
end, {
|
||||
desc = 'Test Claude diff review with current changes',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeInstallHooks', function()
|
||||
M.install_hooks()
|
||||
end, {
|
||||
desc = 'Install Claude Code hooks for this project',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeUninstallHooks', function()
|
||||
M.uninstall_hooks()
|
||||
end, {
|
||||
desc = 'Uninstall Claude Code hooks for this project',
|
||||
})
|
||||
|
||||
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
|
||||
M.claude_edited_files = {}
|
||||
|
||||
-- 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',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeAcceptAll', function()
|
||||
local inline_diff = require 'nvim-claude.inline-diff'
|
||||
inline_diff.accept_all_files()
|
||||
end, {
|
||||
desc = 'Accept all Claude diffs across all files',
|
||||
})
|
||||
|
||||
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)',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeDebugTracking', function()
|
||||
-- Debug command to show current tracking state
|
||||
local inline_diff = require 'nvim-claude.inline-diff'
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
local utils = require 'nvim-claude.utils'
|
||||
|
||||
vim.notify('=== Claude Tracking Debug ===', vim.log.levels.INFO)
|
||||
vim.notify('Stable baseline: ' .. (M.stable_baseline_ref or 'none'), vim.log.levels.INFO)
|
||||
vim.notify('Persistence stash ref: ' .. (persistence.current_stash_ref or 'none'), vim.log.levels.INFO)
|
||||
vim.notify('Claude edited files: ' .. vim.inspect(M.claude_edited_files), vim.log.levels.INFO)
|
||||
vim.notify('Diff files: ' .. vim.inspect(vim.tbl_keys(inline_diff.diff_files)), vim.log.levels.INFO)
|
||||
vim.notify('Active diffs: ' .. vim.inspect(vim.tbl_keys(inline_diff.active_diffs)), vim.log.levels.INFO)
|
||||
|
||||
-- Check current file
|
||||
local current_file = vim.api.nvim_buf_get_name(0)
|
||||
local git_root = utils.get_project_root()
|
||||
if git_root then
|
||||
local relative_path = current_file:gsub('^' .. vim.pesc(git_root) .. '/', '')
|
||||
vim.notify('Current file relative path: ' .. relative_path, vim.log.levels.INFO)
|
||||
vim.notify('Is tracked: ' .. tostring(M.claude_edited_files[relative_path] ~= nil), vim.log.levels.INFO)
|
||||
end
|
||||
end, {
|
||||
desc = 'Debug Claude tracking state',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeRestoreState', function()
|
||||
-- Manually restore the state
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
local restored = persistence.restore_diffs()
|
||||
|
||||
if persistence.current_stash_ref then
|
||||
M.stable_baseline_ref = persistence.current_stash_ref
|
||||
end
|
||||
|
||||
vim.notify('Manually restored state', vim.log.levels.INFO)
|
||||
end, {
|
||||
desc = 'Manually restore Claude diff state',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeCleanStaleTracking', function()
|
||||
local utils = require 'nvim-claude.utils'
|
||||
local persistence = require 'nvim-claude.inline-diff-persistence'
|
||||
local git_root = utils.get_project_root()
|
||||
|
||||
if not git_root or not M.stable_baseline_ref then
|
||||
vim.notify('No git root or baseline found', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local cleaned_count = 0
|
||||
local files_to_remove = {}
|
||||
|
||||
-- Check each tracked file for actual differences
|
||||
for file_path, _ in pairs(M.claude_edited_files) do
|
||||
local diff_cmd = string.format('cd "%s" && git diff %s -- "%s" 2>/dev/null', git_root, M.stable_baseline_ref, file_path)
|
||||
local diff_output = utils.exec(diff_cmd)
|
||||
|
||||
if not diff_output or diff_output == '' then
|
||||
-- No differences, remove from tracking
|
||||
table.insert(files_to_remove, file_path)
|
||||
cleaned_count = cleaned_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove files with no differences
|
||||
for _, file_path in ipairs(files_to_remove) do
|
||||
M.claude_edited_files[file_path] = nil
|
||||
end
|
||||
|
||||
-- Save updated state if we have a persistence stash ref
|
||||
if persistence.current_stash_ref then
|
||||
persistence.save_state({ stash_ref = persistence.current_stash_ref })
|
||||
end
|
||||
|
||||
vim.notify(string.format('Cleaned %d stale tracked files', cleaned_count), vim.log.levels.INFO)
|
||||
end, {
|
||||
desc = 'Clean up stale Claude file tracking',
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command('ClaudeUntrackFile', function()
|
||||
-- Remove current file from Claude tracking
|
||||
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 file_path = vim.api.nvim_buf_get_name(0)
|
||||
local relative_path = file_path:gsub(git_root .. '/', '')
|
||||
|
||||
if M.claude_edited_files[relative_path] then
|
||||
M.claude_edited_files[relative_path] = nil
|
||||
vim.notify('Removed ' .. relative_path .. ' from Claude tracking', vim.log.levels.INFO)
|
||||
|
||||
-- Also close any active inline diff for this buffer
|
||||
local inline_diff = require 'nvim-claude.inline-diff'
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
if inline_diff.has_active_diff(bufnr) then
|
||||
inline_diff.close_inline_diff(bufnr)
|
||||
end
|
||||
else
|
||||
vim.notify(relative_path .. ' is not in Claude tracking', vim.log.levels.INFO)
|
||||
end
|
||||
end, {
|
||||
desc = 'Remove current file from Claude edited files tracking',
|
||||
})
|
||||
end
|
||||
|
||||
-- Cleanup old temp files (no longer cleans up commits)
|
||||
function M.cleanup_old_files()
|
||||
-- Clean up old temp files
|
||||
local temp_files = {
|
||||
'/tmp/claude-pre-edit-commit',
|
||||
'/tmp/claude-baseline-commit',
|
||||
'/tmp/claude-last-snapshot',
|
||||
'/tmp/claude-hook-test.log',
|
||||
}
|
||||
|
||||
for _, file in ipairs(temp_files) do
|
||||
if vim.fn.filereadable(file) == 1 then
|
||||
vim.fn.delete(file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
@ -1,153 +0,0 @@
|
||||
-- nvim-claude: Claude integration for Neovim with tmux workflow
|
||||
local M = {}
|
||||
|
||||
-- Default configuration
|
||||
M.config = {
|
||||
tmux = {
|
||||
split_direction = 'h', -- horizontal split
|
||||
split_size = 40, -- 40% width
|
||||
session_prefix = 'claude-',
|
||||
pane_title = 'claude-chat',
|
||||
},
|
||||
agents = {
|
||||
work_dir = '.agent-work',
|
||||
use_worktrees = true,
|
||||
auto_gitignore = true,
|
||||
max_agents = 5,
|
||||
cleanup_days = 7,
|
||||
},
|
||||
ui = {
|
||||
float_diff = true,
|
||||
telescope_preview = true,
|
||||
status_line = true,
|
||||
},
|
||||
mappings = {
|
||||
prefix = '<leader>c',
|
||||
quick_prefix = '<C-c>',
|
||||
},
|
||||
}
|
||||
|
||||
-- Validate configuration
|
||||
local function validate_config(config)
|
||||
local ok = true
|
||||
local errors = {}
|
||||
|
||||
-- Validate tmux settings
|
||||
if config.tmux then
|
||||
if config.tmux.split_direction and
|
||||
config.tmux.split_direction ~= 'h' and
|
||||
config.tmux.split_direction ~= 'v' then
|
||||
table.insert(errors, "tmux.split_direction must be 'h' or 'v'")
|
||||
ok = false
|
||||
end
|
||||
|
||||
if config.tmux.split_size and
|
||||
(type(config.tmux.split_size) ~= 'number' or
|
||||
config.tmux.split_size < 1 or
|
||||
config.tmux.split_size > 99) then
|
||||
table.insert(errors, "tmux.split_size must be a number between 1 and 99")
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Validate agent settings
|
||||
if config.agents then
|
||||
if config.agents.max_agents and
|
||||
(type(config.agents.max_agents) ~= 'number' or
|
||||
config.agents.max_agents < 1) then
|
||||
table.insert(errors, "agents.max_agents must be a positive number")
|
||||
ok = false
|
||||
end
|
||||
|
||||
if config.agents.cleanup_days and
|
||||
(type(config.agents.cleanup_days) ~= 'number' or
|
||||
config.agents.cleanup_days < 0) then
|
||||
table.insert(errors, "agents.cleanup_days must be a non-negative number")
|
||||
ok = false
|
||||
end
|
||||
|
||||
if config.agents.work_dir and
|
||||
(type(config.agents.work_dir) ~= 'string' or
|
||||
config.agents.work_dir:match('^/') or
|
||||
config.agents.work_dir:match('%.%.')) then
|
||||
table.insert(errors, "agents.work_dir must be a relative path without '..'")
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Validate mappings
|
||||
if config.mappings then
|
||||
if config.mappings.prefix and
|
||||
type(config.mappings.prefix) ~= 'string' then
|
||||
table.insert(errors, "mappings.prefix must be a string")
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
|
||||
return ok, errors
|
||||
end
|
||||
|
||||
-- Merge user config with defaults
|
||||
local function merge_config(user_config)
|
||||
local merged = vim.tbl_deep_extend('force', M.config, user_config or {})
|
||||
|
||||
-- Validate the merged config
|
||||
local ok, errors = validate_config(merged)
|
||||
if not ok then
|
||||
vim.notify('nvim-claude: Configuration errors:', vim.log.levels.ERROR)
|
||||
for _, err in ipairs(errors) do
|
||||
vim.notify(' - ' .. err, vim.log.levels.ERROR)
|
||||
end
|
||||
vim.notify('Using default configuration', vim.log.levels.WARN)
|
||||
return M.config
|
||||
end
|
||||
|
||||
return merged
|
||||
end
|
||||
|
||||
-- Plugin setup
|
||||
function M.setup(user_config)
|
||||
M.config = merge_config(user_config)
|
||||
|
||||
-- Force reload modules to ensure latest code
|
||||
package.loaded['nvim-claude.hooks'] = nil
|
||||
package.loaded['nvim-claude.diff-review'] = nil
|
||||
|
||||
-- Load submodules
|
||||
M.tmux = require('nvim-claude.tmux')
|
||||
M.git = require('nvim-claude.git')
|
||||
M.utils = require('nvim-claude.utils')
|
||||
M.commands = require('nvim-claude.commands')
|
||||
M.registry = require('nvim-claude.registry')
|
||||
M.hooks = require('nvim-claude.hooks')
|
||||
M.diff_review = require('nvim-claude.diff-review')
|
||||
M.settings_updater = require('nvim-claude.settings-updater')
|
||||
|
||||
-- Initialize submodules with config
|
||||
M.tmux.setup(M.config.tmux)
|
||||
M.git.setup(M.config.agents)
|
||||
M.registry.setup(M.config.agents)
|
||||
M.hooks.setup()
|
||||
M.diff_review.setup()
|
||||
M.settings_updater.setup()
|
||||
|
||||
-- Set up commands
|
||||
M.commands.setup(M)
|
||||
M.hooks.setup_commands()
|
||||
|
||||
-- Auto-install hooks if we're in a git repository
|
||||
vim.defer_fn(function()
|
||||
if M.utils.get_project_root() then
|
||||
M.hooks.install_hooks()
|
||||
end
|
||||
end, 100)
|
||||
|
||||
-- Set up keymappings if enabled
|
||||
if M.config.mappings then
|
||||
require('nvim-claude.mappings').setup(M.config.mappings, M.commands)
|
||||
end
|
||||
|
||||
vim.notify('nvim-claude loaded', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
return M
|
@ -1,57 +0,0 @@
|
||||
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
|
@ -1,314 +0,0 @@
|
||||
-- Persistence layer for inline diffs
|
||||
-- Manages saving/loading diff state across neovim sessions without polluting git history
|
||||
|
||||
local M = {}
|
||||
local utils = require('nvim-claude.utils')
|
||||
|
||||
-- State file location
|
||||
M.state_file = vim.fn.stdpath('data') .. '/nvim-claude-inline-diff-state.json'
|
||||
|
||||
-- Save current diff state
|
||||
function M.save_state(diff_data)
|
||||
-- Structure:
|
||||
-- {
|
||||
-- version: 1,
|
||||
-- timestamp: <unix_timestamp>,
|
||||
-- stash_ref: "stash@{0}",
|
||||
-- files: {
|
||||
-- "/path/to/file": {
|
||||
-- original_content: "...",
|
||||
-- hunks: [...],
|
||||
-- applied_hunks: {...}
|
||||
-- }
|
||||
-- }
|
||||
-- }
|
||||
|
||||
local hooks = require('nvim-claude.hooks')
|
||||
local inline_diff = require('nvim-claude.inline-diff')
|
||||
|
||||
local state = {
|
||||
version = 1,
|
||||
timestamp = os.time(),
|
||||
stash_ref = diff_data.stash_ref,
|
||||
claude_edited_files = hooks.claude_edited_files or {},
|
||||
diff_files = {}, -- Add diff_files to persistence
|
||||
files = {}
|
||||
}
|
||||
|
||||
-- Save all diff files (both opened and unopened)
|
||||
for file_path, bufnr in pairs(inline_diff.diff_files) do
|
||||
state.diff_files[file_path] = bufnr
|
||||
end
|
||||
|
||||
-- Collect state from all buffers with active diffs
|
||||
for file_path, bufnr in pairs(inline_diff.diff_files) do
|
||||
if inline_diff.active_diffs[bufnr] then
|
||||
local diff = inline_diff.active_diffs[bufnr]
|
||||
state.files[file_path] = {
|
||||
original_content = inline_diff.original_content[bufnr],
|
||||
hunks = diff.hunks,
|
||||
applied_hunks = diff.applied_hunks or {},
|
||||
new_content = diff.new_content
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Save to file
|
||||
local success, err = utils.write_json(M.state_file, state)
|
||||
if not success then
|
||||
vim.notify('Failed to save inline diff state: ' .. err, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Load saved diff state
|
||||
function M.load_state()
|
||||
if not utils.file_exists(M.state_file) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local state, err = utils.read_json(M.state_file)
|
||||
if not state then
|
||||
vim.notify('Failed to load inline diff state: ' .. err, vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Validate version
|
||||
if state.version ~= 1 then
|
||||
vim.notify('Incompatible inline diff state version', vim.log.levels.WARN)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check if stash still exists
|
||||
if state.stash_ref then
|
||||
local cmd = string.format('git stash list | grep -q "%s"', state.stash_ref:gsub("{", "\\{"):gsub("}", "\\}"))
|
||||
local result = os.execute(cmd)
|
||||
if result ~= 0 then
|
||||
vim.notify('Saved stash no longer exists: ' .. state.stash_ref, vim.log.levels.WARN)
|
||||
M.clear_state()
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
-- Clear saved state
|
||||
function M.clear_state()
|
||||
if utils.file_exists(M.state_file) then
|
||||
os.remove(M.state_file)
|
||||
end
|
||||
end
|
||||
|
||||
-- Restore diffs from saved state
|
||||
function M.restore_diffs()
|
||||
local state = M.load_state()
|
||||
if not state then
|
||||
return false
|
||||
end
|
||||
|
||||
local inline_diff = require('nvim-claude.inline-diff')
|
||||
local restored_count = 0
|
||||
|
||||
-- Restore diffs for each file
|
||||
for file_path, file_state in pairs(state.files) do
|
||||
-- Check if file exists and hasn't changed since the diff was created
|
||||
if utils.file_exists(file_path) then
|
||||
-- Read current content
|
||||
local current_content = utils.read_file(file_path)
|
||||
|
||||
-- Check if the file matches what we expect (either original or with applied changes)
|
||||
-- This handles the case where some hunks were accepted
|
||||
if current_content then
|
||||
-- Find or create buffer for this file
|
||||
local bufnr = vim.fn.bufnr(file_path)
|
||||
if bufnr == -1 then
|
||||
-- File not loaded, we'll restore when it's opened
|
||||
-- Store in a pending restores table
|
||||
M.pending_restores = M.pending_restores or {}
|
||||
M.pending_restores[file_path] = file_state
|
||||
else
|
||||
-- Restore the diff visualization
|
||||
inline_diff.original_content[bufnr] = file_state.original_content
|
||||
inline_diff.diff_files[file_path] = bufnr
|
||||
inline_diff.active_diffs[bufnr] = {
|
||||
hunks = file_state.hunks,
|
||||
new_content = file_state.new_content,
|
||||
current_hunk = 1,
|
||||
applied_hunks = file_state.applied_hunks or {}
|
||||
}
|
||||
|
||||
-- Apply visualization
|
||||
inline_diff.apply_diff_visualization(bufnr)
|
||||
inline_diff.setup_inline_keymaps(bufnr)
|
||||
|
||||
restored_count = restored_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if restored_count > 0 then
|
||||
-- Silent restore - no notification
|
||||
end
|
||||
|
||||
-- Store the stash reference for future operations
|
||||
M.current_stash_ref = state.stash_ref
|
||||
|
||||
if M.current_stash_ref then
|
||||
vim.notify('Restored stash reference: ' .. M.current_stash_ref, vim.log.levels.DEBUG)
|
||||
end
|
||||
|
||||
-- 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
|
||||
vim.notify(string.format('Restored %d Claude edited files', vim.tbl_count(state.claude_edited_files)), vim.log.levels.DEBUG)
|
||||
end
|
||||
|
||||
-- Restore diff_files for unopened files
|
||||
if state.diff_files then
|
||||
for file_path, bufnr in pairs(state.diff_files) do
|
||||
-- Only restore if not already restored as an active diff
|
||||
if not inline_diff.diff_files[file_path] then
|
||||
-- Use -1 to indicate unopened file
|
||||
inline_diff.diff_files[file_path] = bufnr == -1 and -1 or -1
|
||||
end
|
||||
end
|
||||
vim.notify(string.format('Restored %d diff files', vim.tbl_count(state.diff_files)), vim.log.levels.DEBUG)
|
||||
end
|
||||
|
||||
-- Also populate diff_files from claude_edited_files if needed
|
||||
-- This ensures <leader>ci works even if diff_files wasn't properly saved
|
||||
if state.claude_edited_files then
|
||||
local utils = require('nvim-claude.utils')
|
||||
local git_root = utils.get_project_root()
|
||||
|
||||
if git_root then
|
||||
for relative_path, _ in pairs(state.claude_edited_files) do
|
||||
local full_path = git_root .. '/' .. relative_path
|
||||
-- Only add if not already in diff_files
|
||||
if not inline_diff.diff_files[full_path] then
|
||||
inline_diff.diff_files[full_path] = -1 -- Mark as unopened
|
||||
vim.notify('Added ' .. relative_path .. ' to diff_files from claude_edited_files', vim.log.levels.DEBUG)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check for pending restores when a buffer is loaded
|
||||
function M.check_pending_restore(bufnr)
|
||||
if not M.pending_restores then
|
||||
return
|
||||
end
|
||||
|
||||
local file_path = vim.api.nvim_buf_get_name(bufnr)
|
||||
local file_state = M.pending_restores[file_path]
|
||||
|
||||
if file_state then
|
||||
local inline_diff = require('nvim-claude.inline-diff')
|
||||
|
||||
-- Restore the diff for this buffer
|
||||
inline_diff.original_content[bufnr] = file_state.original_content
|
||||
inline_diff.diff_files[file_path] = bufnr
|
||||
inline_diff.active_diffs[bufnr] = {
|
||||
hunks = file_state.hunks,
|
||||
new_content = file_state.new_content,
|
||||
current_hunk = 1,
|
||||
applied_hunks = file_state.applied_hunks or {}
|
||||
}
|
||||
|
||||
-- Apply visualization
|
||||
inline_diff.apply_diff_visualization(bufnr)
|
||||
inline_diff.setup_inline_keymaps(bufnr)
|
||||
|
||||
-- Remove from pending
|
||||
M.pending_restores[file_path] = nil
|
||||
|
||||
-- Silent restore - no notification
|
||||
end
|
||||
end
|
||||
|
||||
-- Create a stash of current changes (instead of baseline commit)
|
||||
function M.create_stash(message)
|
||||
local utils = require('nvim-claude.utils')
|
||||
message = message or 'nvim-claude: pre-edit state'
|
||||
|
||||
-- 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)
|
||||
|
||||
-- Get the stash reference
|
||||
local stash_list = utils.exec('git stash list -n 1')
|
||||
if stash_list then
|
||||
local stash_ref = stash_list:match('^(stash@{%d+})')
|
||||
return stash_ref
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Setup autocmds for persistence
|
||||
function M.setup_autocmds()
|
||||
local group = vim.api.nvim_create_augroup('NvimClaudeInlineDiffPersistence', { clear = true })
|
||||
|
||||
-- Save state before exiting vim
|
||||
vim.api.nvim_create_autocmd('VimLeavePre', {
|
||||
group = group,
|
||||
callback = function()
|
||||
local inline_diff = require('nvim-claude.inline-diff')
|
||||
-- Only save if there are active diffs
|
||||
local has_active_diffs = false
|
||||
for _, diff in pairs(inline_diff.active_diffs) do
|
||||
if diff then
|
||||
has_active_diffs = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
-- Check for pending restores when buffers are loaded
|
||||
vim.api.nvim_create_autocmd('BufReadPost', {
|
||||
group = group,
|
||||
callback = function(ev)
|
||||
M.check_pending_restore(ev.buf)
|
||||
end
|
||||
})
|
||||
|
||||
-- Auto-restore on VimEnter
|
||||
vim.api.nvim_create_autocmd('VimEnter', {
|
||||
group = group,
|
||||
once = true,
|
||||
callback = function()
|
||||
-- Delay slightly to ensure everything is loaded
|
||||
vim.defer_fn(function()
|
||||
M.restore_diffs()
|
||||
end, 100)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
File diff suppressed because it is too large
Load Diff
@ -1,113 +0,0 @@
|
||||
-- Keybinding mappings for nvim-claude
|
||||
local M = {}
|
||||
|
||||
function M.setup(config, commands)
|
||||
local prefix = config.prefix or '<leader>c'
|
||||
|
||||
-- Basic commands
|
||||
vim.keymap.set('n', prefix .. 'c', ':ClaudeChat<CR>', {
|
||||
desc = 'Open Claude chat',
|
||||
silent = true
|
||||
})
|
||||
|
||||
vim.keymap.set('n', prefix .. 's', ':ClaudeSendBuffer<CR>', {
|
||||
desc = 'Send buffer to Claude',
|
||||
silent = true
|
||||
})
|
||||
|
||||
vim.keymap.set('v', prefix .. 'v', ':ClaudeSendSelection<CR>', {
|
||||
desc = 'Send selection to Claude',
|
||||
silent = true
|
||||
})
|
||||
|
||||
vim.keymap.set('n', prefix .. 'h', ':ClaudeSendHunk<CR>', {
|
||||
desc = 'Send git hunk to Claude',
|
||||
silent = true
|
||||
})
|
||||
|
||||
vim.keymap.set('n', prefix .. 'b', ':ClaudeBg ', {
|
||||
desc = 'Start background agent',
|
||||
silent = false -- Allow user to type the task
|
||||
})
|
||||
|
||||
vim.keymap.set('n', prefix .. 'l', ':ClaudeAgents<CR>', {
|
||||
desc = 'List agents',
|
||||
silent = true
|
||||
})
|
||||
|
||||
vim.keymap.set('n', prefix .. 'k', ':ClaudeKill<CR>', {
|
||||
desc = 'Kill agent',
|
||||
silent = true
|
||||
})
|
||||
|
||||
vim.keymap.set('n', prefix .. 'x', ':ClaudeClean<CR>', {
|
||||
desc = 'Clean old agents',
|
||||
silent = true
|
||||
})
|
||||
|
||||
-- Register with which-key if available
|
||||
local ok, which_key = pcall(require, 'which-key')
|
||||
if ok then
|
||||
which_key.register({
|
||||
[prefix] = {
|
||||
name = 'Claude',
|
||||
c = { 'Chat' },
|
||||
s = { 'Send Buffer' },
|
||||
v = { 'Send Selection' },
|
||||
h = { 'Send Git Hunk' },
|
||||
b = { 'Background Agent' },
|
||||
l = { 'List Agents' },
|
||||
k = { 'Kill Agent' },
|
||||
x = { 'Clean Old Agents' },
|
||||
i = { 'List files with diffs' },
|
||||
},
|
||||
['<leader>i'] = {
|
||||
name = 'Inline Diffs',
|
||||
a = { 'Accept current hunk' },
|
||||
r = { 'Reject current hunk' },
|
||||
A = { 'Accept all hunks in file' },
|
||||
R = { 'Reject all hunks in file' },
|
||||
AA = { 'Accept ALL diffs in ALL files' },
|
||||
q = { 'Close inline diff' },
|
||||
l = { '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
|
||||
})
|
||||
|
||||
-- Global keymap to accept all diffs across all files
|
||||
vim.keymap.set('n', '<leader>iAA', function()
|
||||
local inline_diff = require('nvim-claude.inline-diff')
|
||||
inline_diff.accept_all_files()
|
||||
end, {
|
||||
desc = 'Accept ALL Claude diffs in ALL files',
|
||||
silent = true
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
@ -1,248 +0,0 @@
|
||||
-- Agent registry module for nvim-claude
|
||||
local M = {}
|
||||
local utils = require('nvim-claude.utils')
|
||||
|
||||
-- Registry data
|
||||
M.agents = {}
|
||||
M.registry_path = nil
|
||||
|
||||
-- Initialize registry
|
||||
function M.setup(config)
|
||||
-- Set up registry directory
|
||||
local data_dir = vim.fn.stdpath('data') .. '/nvim-claude'
|
||||
utils.ensure_dir(data_dir)
|
||||
|
||||
M.registry_path = data_dir .. '/registry.json'
|
||||
|
||||
-- Load existing registry
|
||||
M.load()
|
||||
end
|
||||
|
||||
-- Load registry from disk
|
||||
function M.load()
|
||||
vim.notify('registry.load() called', vim.log.levels.DEBUG)
|
||||
local content = utils.read_file(M.registry_path)
|
||||
if content then
|
||||
vim.notify(string.format('registry.load: Read %d bytes from %s', #content, M.registry_path), vim.log.levels.DEBUG)
|
||||
local ok, data = pcall(vim.json.decode, content)
|
||||
if ok and type(data) == 'table' then
|
||||
local agent_count = vim.tbl_count(data)
|
||||
vim.notify(string.format('registry.load: Decoded %d agents from JSON', agent_count), vim.log.levels.DEBUG)
|
||||
M.agents = data
|
||||
M.validate_agents()
|
||||
else
|
||||
vim.notify('registry.load: Failed to decode JSON, clearing agents', vim.log.levels.WARN)
|
||||
M.agents = {}
|
||||
end
|
||||
else
|
||||
vim.notify('registry.load: No content read from file, clearing agents', vim.log.levels.WARN)
|
||||
M.agents = {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Save registry to disk
|
||||
function M.save()
|
||||
if not M.registry_path then return false end
|
||||
|
||||
local content = vim.json.encode(M.agents)
|
||||
return utils.write_file(M.registry_path, content)
|
||||
end
|
||||
|
||||
-- Validate agents (remove stale entries)
|
||||
function M.validate_agents()
|
||||
local valid_agents = {}
|
||||
local now = os.time()
|
||||
|
||||
|
||||
for id, agent in pairs(M.agents) do
|
||||
-- Check if agent directory still exists
|
||||
local mission_log_path = agent.work_dir .. '/mission.log'
|
||||
local mission_exists = utils.file_exists(mission_log_path)
|
||||
|
||||
|
||||
if mission_exists then
|
||||
-- Check if tmux window still exists
|
||||
local window_exists = M.check_window_exists(agent.window_id)
|
||||
|
||||
if window_exists then
|
||||
agent.status = 'active'
|
||||
|
||||
-- Update progress from file for active agents
|
||||
local progress_file = agent.work_dir .. '/progress.txt'
|
||||
local progress_content = utils.read_file(progress_file)
|
||||
if progress_content and progress_content ~= '' then
|
||||
agent.progress = progress_content:gsub('\n$', '') -- Remove trailing newline
|
||||
end
|
||||
|
||||
valid_agents[id] = agent
|
||||
else
|
||||
-- Window closed, mark as completed
|
||||
agent.status = 'completed'
|
||||
agent.end_time = agent.end_time or now
|
||||
valid_agents[id] = agent
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.agents = valid_agents
|
||||
M.save()
|
||||
end
|
||||
|
||||
-- Check if tmux window exists
|
||||
function M.check_window_exists(window_id)
|
||||
if not window_id then return false end
|
||||
|
||||
local cmd = string.format("tmux list-windows -F '#{window_id}' | grep -q '^%s$'", window_id)
|
||||
local result = os.execute(cmd)
|
||||
return result == 0
|
||||
end
|
||||
|
||||
-- Register a new agent
|
||||
function M.register(task, work_dir, window_id, window_name, fork_info)
|
||||
local id = utils.timestamp() .. '-' .. math.random(1000, 9999)
|
||||
local agent = {
|
||||
id = id,
|
||||
task = task,
|
||||
work_dir = work_dir,
|
||||
window_id = window_id,
|
||||
window_name = window_name,
|
||||
start_time = os.time(),
|
||||
status = 'active',
|
||||
project_root = utils.get_project_root(),
|
||||
progress = 'Starting...', -- Add progress field
|
||||
last_update = os.time(),
|
||||
fork_info = fork_info, -- Store branch/stash info
|
||||
}
|
||||
|
||||
M.agents[id] = agent
|
||||
M.save()
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
-- Get agent by ID
|
||||
function M.get(id)
|
||||
return M.agents[id]
|
||||
end
|
||||
|
||||
-- Get all agents for current project
|
||||
function M.get_project_agents()
|
||||
-- Ensure registry is loaded
|
||||
if not M.agents or vim.tbl_isempty(M.agents) then
|
||||
M.load()
|
||||
end
|
||||
|
||||
local project_root = utils.get_project_root()
|
||||
local project_agents = {}
|
||||
|
||||
for id, agent in pairs(M.agents) do
|
||||
if agent.project_root == project_root then
|
||||
-- Include the registry ID with the agent
|
||||
agent._registry_id = id
|
||||
table.insert(project_agents, agent)
|
||||
end
|
||||
end
|
||||
|
||||
return project_agents
|
||||
end
|
||||
|
||||
-- Get active agents count
|
||||
function M.get_active_count()
|
||||
local count = 0
|
||||
for _, agent in pairs(M.agents) do
|
||||
if agent.status == 'active' then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Update agent status
|
||||
function M.update_status(id, status)
|
||||
if M.agents[id] then
|
||||
M.agents[id].status = status
|
||||
if status == 'completed' or status == 'failed' then
|
||||
M.agents[id].end_time = os.time()
|
||||
end
|
||||
M.agents[id].last_update = os.time()
|
||||
M.save()
|
||||
end
|
||||
end
|
||||
|
||||
-- Update agent progress
|
||||
function M.update_progress(id, progress)
|
||||
if M.agents[id] then
|
||||
M.agents[id].progress = progress
|
||||
M.agents[id].last_update = os.time()
|
||||
M.save()
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove agent
|
||||
function M.remove(id)
|
||||
M.agents[id] = nil
|
||||
M.save()
|
||||
end
|
||||
|
||||
-- Clean up old agents
|
||||
function M.cleanup(days)
|
||||
if not days or days < 0 then return end
|
||||
|
||||
local cutoff = os.time() - (days * 24 * 60 * 60)
|
||||
local removed = 0
|
||||
|
||||
for id, agent in pairs(M.agents) do
|
||||
if agent.status ~= 'active' and agent.end_time and agent.end_time < cutoff then
|
||||
-- Remove work directory
|
||||
if agent.work_dir and utils.file_exists(agent.work_dir) then
|
||||
local cmd = string.format('rm -rf "%s"', agent.work_dir)
|
||||
utils.exec(cmd)
|
||||
end
|
||||
|
||||
M.agents[id] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
|
||||
if removed > 0 then
|
||||
M.save()
|
||||
end
|
||||
|
||||
return removed
|
||||
end
|
||||
|
||||
-- Format agent for display
|
||||
function M.format_agent(agent)
|
||||
local age = os.difftime(os.time(), agent.start_time)
|
||||
local age_str
|
||||
|
||||
if age < 60 then
|
||||
age_str = string.format('%ds', age)
|
||||
elseif age < 3600 then
|
||||
age_str = string.format('%dm', math.floor(age / 60))
|
||||
elseif age < 86400 then
|
||||
age_str = string.format('%dh', math.floor(age / 3600))
|
||||
else
|
||||
age_str = string.format('%dd', math.floor(age / 86400))
|
||||
end
|
||||
|
||||
local progress_str = ''
|
||||
if agent.progress and agent.status == 'active' then
|
||||
progress_str = string.format(' | %s', agent.progress)
|
||||
end
|
||||
|
||||
-- Clean up task to single line
|
||||
local task_line = agent.task:match('[^\n]*') or agent.task
|
||||
local task_preview = task_line:sub(1, 50) .. (task_line:len() > 50 and '...' or '')
|
||||
|
||||
return string.format(
|
||||
'[%s] %s (%s) - %s%s',
|
||||
agent.status:upper(),
|
||||
task_preview,
|
||||
age_str,
|
||||
agent.window_name or 'unknown',
|
||||
progress_str
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
@ -1,112 +0,0 @@
|
||||
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
|
@ -1,67 +0,0 @@
|
||||
-- Statusline components for nvim-claude
|
||||
local M = {}
|
||||
|
||||
-- Get active agent count and summary
|
||||
function M.get_agent_status()
|
||||
local registry = require('nvim-claude.registry')
|
||||
|
||||
-- Validate agents to update their status
|
||||
registry.validate_agents()
|
||||
|
||||
local agents = registry.get_project_agents()
|
||||
local active_count = 0
|
||||
local latest_progress = nil
|
||||
local latest_task = nil
|
||||
|
||||
for _, agent in ipairs(agents) do
|
||||
if agent.status == 'active' then
|
||||
active_count = active_count + 1
|
||||
-- Get the most recently updated active agent
|
||||
if not latest_progress or (agent.last_update and agent.last_update > (latest_progress.last_update or 0)) then
|
||||
latest_progress = agent.progress
|
||||
latest_task = agent.task
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if active_count == 0 then
|
||||
return ''
|
||||
elseif active_count == 1 and latest_progress then
|
||||
-- Show single agent progress
|
||||
local task_short = latest_task
|
||||
if #latest_task > 20 then
|
||||
task_short = latest_task:sub(1, 17) .. '...'
|
||||
end
|
||||
return string.format('🤖 %s: %s', task_short, latest_progress)
|
||||
else
|
||||
-- Show count of multiple agents
|
||||
return string.format('🤖 %d agents', active_count)
|
||||
end
|
||||
end
|
||||
|
||||
-- Lualine component
|
||||
function M.lualine_component()
|
||||
return {
|
||||
M.get_agent_status,
|
||||
cond = function()
|
||||
-- Only show if there are active agents
|
||||
local status = M.get_agent_status()
|
||||
return status ~= ''
|
||||
end,
|
||||
on_click = function()
|
||||
-- Open agent list on click
|
||||
vim.cmd('ClaudeAgents')
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Simple string function for custom statuslines
|
||||
function M.statusline()
|
||||
local status = M.get_agent_status()
|
||||
if status ~= '' then
|
||||
return ' ' .. status .. ' '
|
||||
end
|
||||
return ''
|
||||
end
|
||||
|
||||
return M
|
@ -1,271 +0,0 @@
|
||||
-- Tmux interaction module for nvim-claude
|
||||
local M = {}
|
||||
local utils = require('nvim-claude.utils')
|
||||
|
||||
M.config = {}
|
||||
|
||||
function M.setup(config)
|
||||
M.config = config or {}
|
||||
end
|
||||
|
||||
-- Find Claude pane by checking running command
|
||||
function M.find_claude_pane()
|
||||
-- Get list of panes with their PIDs and current command
|
||||
local cmd = "tmux list-panes -F '#{pane_id}:#{pane_pid}:#{pane_title}:#{pane_current_command}'"
|
||||
local result = utils.exec(cmd)
|
||||
|
||||
if result and result ~= '' then
|
||||
-- Check each pane
|
||||
for line in result:gmatch('[^\n]+') do
|
||||
local pane_id, pane_pid, pane_title, pane_cmd = line:match('^([^:]+):([^:]+):([^:]*):(.*)$')
|
||||
if pane_id and pane_pid then
|
||||
-- Check by title first (our created panes)
|
||||
if pane_title and pane_title == (M.config.pane_title or 'claude-chat') then
|
||||
return pane_id
|
||||
end
|
||||
|
||||
-- Check if title contains Claude Code indicators (Anthropic symbol, "claude", "Claude")
|
||||
if pane_title and (pane_title:match('✳') or pane_title:match('[Cc]laude')) then
|
||||
return pane_id
|
||||
end
|
||||
|
||||
-- Check if current command is claude-related
|
||||
if pane_cmd and pane_cmd:match('claude') then
|
||||
return pane_id
|
||||
end
|
||||
|
||||
-- Check if any child process of this pane is running claude or claude-code
|
||||
-- This handles cases where claude is run under a shell
|
||||
local check_cmd = string.format(
|
||||
"ps -ef | awk '$3 == %s' | grep -c -E '(claude|claude-code)' 2>/dev/null",
|
||||
pane_pid
|
||||
)
|
||||
local count_result = utils.exec(check_cmd)
|
||||
if count_result and tonumber(count_result) and tonumber(count_result) > 0 then
|
||||
return pane_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Create new tmux pane for Claude (or return existing)
|
||||
function M.create_pane(command)
|
||||
local existing = M.find_claude_pane()
|
||||
if existing then
|
||||
-- Just select the existing pane, don't create a new one
|
||||
local _, err = utils.exec('tmux select-pane -t ' .. existing)
|
||||
if err then
|
||||
-- Pane might have been closed, continue to create new one
|
||||
vim.notify('Claude pane no longer exists, creating new one', vim.log.levels.INFO)
|
||||
else
|
||||
return existing
|
||||
end
|
||||
end
|
||||
|
||||
-- Build size option safely
|
||||
local size_opt = ''
|
||||
if M.config.split_size and tonumber(M.config.split_size) then
|
||||
local size = tonumber(M.config.split_size)
|
||||
if utils.tmux_supports_length_percent() then
|
||||
size_opt = '-l ' .. tostring(size) .. '%'
|
||||
else
|
||||
size_opt = '-p ' .. tostring(size)
|
||||
end
|
||||
end
|
||||
|
||||
local split_cmd = M.config.split_direction == 'v' and 'split-window -v' or 'split-window -h'
|
||||
|
||||
-- Build command parts to avoid double spaces
|
||||
local parts = { 'tmux', split_cmd }
|
||||
if size_opt ~= '' then table.insert(parts, size_opt) end
|
||||
table.insert(parts, '-P')
|
||||
local cmd = table.concat(parts, ' ')
|
||||
if command then
|
||||
cmd = cmd .. " '" .. command .. "'"
|
||||
end
|
||||
|
||||
local result, err = utils.exec(cmd)
|
||||
if err or not result or result == '' or result:match('error') then
|
||||
vim.notify('nvim-claude: tmux split failed: ' .. (err or result or 'unknown'), vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
local pane_id = result:gsub('\n', '')
|
||||
utils.exec(string.format("tmux select-pane -t %s -T '%s'", pane_id, M.config.pane_title or 'claude-chat'))
|
||||
|
||||
-- Launch command in the new pane if provided
|
||||
if command and command ~= '' then
|
||||
M.send_to_pane(pane_id, command)
|
||||
end
|
||||
|
||||
return pane_id
|
||||
end
|
||||
|
||||
-- Send keys to a pane (single line with Enter)
|
||||
function M.send_to_pane(pane_id, text)
|
||||
if not pane_id then return false end
|
||||
|
||||
-- Escape single quotes in text
|
||||
text = text:gsub("'", "'\"'\"'")
|
||||
|
||||
local cmd = string.format(
|
||||
"tmux send-keys -t %s '%s' Enter",
|
||||
pane_id,
|
||||
text
|
||||
)
|
||||
|
||||
local _, err = utils.exec(cmd)
|
||||
return err == nil
|
||||
end
|
||||
|
||||
-- Send multi-line text to a pane (for batched content)
|
||||
function M.send_text_to_pane(pane_id, text)
|
||||
if not pane_id then return false end
|
||||
|
||||
-- Create a temporary file to hold the text
|
||||
local tmpfile = os.tmpname()
|
||||
local file = io.open(tmpfile, 'w')
|
||||
if not file then
|
||||
vim.notify('Failed to create temporary file for text', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
file:write(text)
|
||||
file:close()
|
||||
|
||||
-- Use tmux load-buffer and paste-buffer to send the content
|
||||
local cmd = string.format(
|
||||
"tmux load-buffer -t %s '%s' && tmux paste-buffer -t %s && rm '%s'",
|
||||
pane_id, tmpfile, pane_id, tmpfile
|
||||
)
|
||||
|
||||
local _, err = utils.exec(cmd)
|
||||
if err then
|
||||
-- Clean up temp file on error
|
||||
os.remove(tmpfile)
|
||||
vim.notify('Failed to send text to pane: ' .. err, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Don't send Enter after pasting to avoid submitting the message
|
||||
-- User can manually submit when ready
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Create new tmux window for agent
|
||||
function M.create_agent_window(name, cwd)
|
||||
local base_cmd = string.format("tmux new-window -n '%s'", name)
|
||||
if cwd and cwd ~= '' then
|
||||
base_cmd = base_cmd .. string.format(" -c '%s'", cwd)
|
||||
end
|
||||
|
||||
local cmd_with_fmt = base_cmd .. " -P -F '#{window_id}'"
|
||||
|
||||
-- Try with -F first (preferred)
|
||||
local result, err = utils.exec(cmd_with_fmt)
|
||||
if not err and result and result ~= '' and not result:match('error') then
|
||||
return result:gsub('\n', '')
|
||||
end
|
||||
|
||||
-- Fallback: simple -P
|
||||
local cmd_simple = base_cmd .. ' -P'
|
||||
result, err = utils.exec(cmd_simple)
|
||||
if not err and result and result ~= '' then
|
||||
return result:gsub('\n', '')
|
||||
end
|
||||
|
||||
vim.notify('nvim-claude: tmux new-window failed: ' .. (err or result or 'unknown'), vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Send keys to an entire window (select-pane 0)
|
||||
function M.send_to_window(window_id, text)
|
||||
if not window_id then return false end
|
||||
text = text:gsub("'", "'\"'\"'")
|
||||
-- Send to the window's active pane (no .0 suffix)
|
||||
local cmd = string.format("tmux send-keys -t %s '%s' Enter", window_id, text)
|
||||
local _, err = utils.exec(cmd)
|
||||
return err == nil
|
||||
end
|
||||
|
||||
-- Switch to window
|
||||
function M.switch_to_window(window_id)
|
||||
local cmd = 'tmux select-window -t ' .. window_id
|
||||
local _, err = utils.exec(cmd)
|
||||
return err == nil
|
||||
end
|
||||
|
||||
-- Kill pane or window
|
||||
function M.kill_pane(pane_id)
|
||||
local cmd = 'tmux kill-pane -t ' .. pane_id
|
||||
local _, err = utils.exec(cmd)
|
||||
return err == nil
|
||||
end
|
||||
|
||||
-- Check if tmux is running
|
||||
function M.is_inside_tmux()
|
||||
-- Check environment variable first
|
||||
if os.getenv('TMUX') then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Fallback: try to get current session name
|
||||
local result = utils.exec('tmux display-message -p "#{session_name}" 2>/dev/null')
|
||||
return result and result ~= '' and not result:match('error')
|
||||
end
|
||||
|
||||
-- Validate tmux availability
|
||||
function M.validate()
|
||||
if not utils.has_tmux() then
|
||||
vim.notify('tmux not found. Please install tmux.', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
if not M.is_inside_tmux() then
|
||||
vim.notify('Not inside tmux session. Please run nvim inside tmux.', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Split a given window and return the new pane id
|
||||
-- direction: 'h' (horizontal/right) or 'v' (vertical/bottom)
|
||||
-- size_percent: number (percentage)
|
||||
function M.split_window(window_id, direction, size_percent)
|
||||
direction = direction == 'v' and '-v' or '-h'
|
||||
|
||||
local size_opt = ''
|
||||
if size_percent and tonumber(size_percent) then
|
||||
local size = tonumber(size_percent)
|
||||
if utils.tmux_supports_length_percent() then
|
||||
size_opt = string.format('-l %s%%', size)
|
||||
else
|
||||
size_opt = string.format('-p %s', size)
|
||||
end
|
||||
end
|
||||
|
||||
-- Build command
|
||||
local parts = {
|
||||
'tmux',
|
||||
'split-window',
|
||||
direction,
|
||||
}
|
||||
if size_opt ~= '' then table.insert(parts, size_opt) end
|
||||
table.insert(parts, '-P -F "#{pane_id}"')
|
||||
table.insert(parts, '-t ' .. window_id)
|
||||
|
||||
local cmd = table.concat(parts, ' ')
|
||||
local pane_id, err = utils.exec(cmd)
|
||||
if err or not pane_id or pane_id == '' then
|
||||
vim.notify('nvim-claude: tmux split-window failed: ' .. (err or pane_id or 'unknown'), vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
return pane_id:gsub('\n', '')
|
||||
end
|
||||
|
||||
return M
|
@ -1,151 +0,0 @@
|
||||
-- Utility functions for nvim-claude
|
||||
-- TEST EDIT #2: Testing multi-file accept
|
||||
local M = {}
|
||||
|
||||
-- Check if we're in a git repository
|
||||
function M.is_git_repo()
|
||||
local handle = io.popen('git rev-parse --git-dir 2>/dev/null')
|
||||
if handle then
|
||||
local result = handle:read('*a')
|
||||
handle:close()
|
||||
return result ~= ''
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Get project root (git root or current working directory)
|
||||
function M.get_project_root()
|
||||
if M.is_git_repo() then
|
||||
local handle = io.popen('git rev-parse --show-toplevel 2>/dev/null')
|
||||
if handle then
|
||||
local root = handle:read('*a'):gsub('\n', '')
|
||||
handle:close()
|
||||
return root
|
||||
end
|
||||
end
|
||||
return vim.fn.getcwd()
|
||||
end
|
||||
|
||||
-- Create directory if it doesn't exist
|
||||
function M.ensure_dir(path)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
if not stat then
|
||||
vim.fn.mkdir(path, 'p')
|
||||
return true
|
||||
end
|
||||
return stat.type == 'directory'
|
||||
end
|
||||
|
||||
-- Read file contents
|
||||
function M.read_file(path)
|
||||
local file = io.open(path, 'r')
|
||||
if not file then return nil end
|
||||
local content = file:read('*a')
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
-- Write file contents
|
||||
function M.write_file(path, content)
|
||||
local file = io.open(path, 'w')
|
||||
if not file then return false end
|
||||
file:write(content)
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if file exists
|
||||
function M.file_exists(path)
|
||||
local stat = vim.loop.fs_stat(path)
|
||||
return stat ~= nil
|
||||
end
|
||||
|
||||
-- Generate timestamp string
|
||||
function M.timestamp()
|
||||
return os.date('%Y-%m-%d-%H%M%S')
|
||||
end
|
||||
|
||||
-- Generate agent directory name
|
||||
function M.agent_dirname(task)
|
||||
-- Sanitize task name for filesystem
|
||||
local safe_task = task:gsub('[^%w%-_]', '-'):gsub('%-+', '-'):sub(1, 50)
|
||||
return string.format('agent-%s-%s', M.timestamp(), safe_task)
|
||||
end
|
||||
|
||||
-- Execute shell command and return output
|
||||
function M.exec(cmd)
|
||||
local handle = io.popen(cmd .. ' 2>&1')
|
||||
if not handle then return nil, 'Failed to execute command' end
|
||||
local result = handle:read('*a')
|
||||
local ok = handle:close()
|
||||
if ok then
|
||||
return result, nil
|
||||
else
|
||||
return result, result
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if tmux is available
|
||||
function M.has_tmux()
|
||||
local result = M.exec('which tmux')
|
||||
return result and result:match('/tmux')
|
||||
end
|
||||
|
||||
-- Get current tmux session
|
||||
function M.get_tmux_session()
|
||||
local result = M.exec('tmux display-message -p "#{session_name}" 2>/dev/null')
|
||||
if result and result ~= '' then
|
||||
return result:gsub('\n', '')
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get tmux version as number (e.g., 3.4) or 0 if unknown
|
||||
function M.tmux_version()
|
||||
local result = M.exec('tmux -V 2>/dev/null')
|
||||
if not result then return 0 end
|
||||
-- Expected output: "tmux 3.4"
|
||||
local ver = result:match('tmux%s+([0-9]+%.[0-9]+)')
|
||||
return tonumber(ver) or 0
|
||||
end
|
||||
|
||||
-- Determine if tmux supports the new -l <percent>% syntax (>= 3.4)
|
||||
function M.tmux_supports_length_percent()
|
||||
return M.tmux_version() >= 3.4
|
||||
end
|
||||
|
||||
-- Write JSON to file
|
||||
function M.write_json(path, data)
|
||||
local success, json = pcall(vim.fn.json_encode, data)
|
||||
if not success then
|
||||
return false, 'Failed to encode JSON: ' .. json
|
||||
end
|
||||
|
||||
local file = io.open(path, 'w')
|
||||
if not file then
|
||||
return false, 'Failed to open file for writing: ' .. path
|
||||
end
|
||||
|
||||
-- Pretty print JSON
|
||||
local formatted = json:gsub('},{', '},\n {'):gsub('\\{', '{\n '):gsub('\\}', '\n}')
|
||||
file:write(formatted)
|
||||
file:close()
|
||||
return true, nil
|
||||
end
|
||||
|
||||
-- Read JSON from file
|
||||
function M.read_json(path)
|
||||
local content = M.read_file(path)
|
||||
if not content then
|
||||
return nil, 'Failed to read file: ' .. path
|
||||
end
|
||||
|
||||
local success, data = pcall(vim.fn.json_decode, content)
|
||||
if not success then
|
||||
return nil, 'Failed to decode JSON: ' .. data
|
||||
end
|
||||
|
||||
return data, nil
|
||||
end
|
||||
|
||||
return M
|
Loading…
Reference in New Issue