claude-pre-edit-1752105425

pull/1635/head
zolinthecow 3 days ago
parent 2a71451ca3
commit cb524a6c14

3
.gitignore vendored

@ -12,3 +12,6 @@ lazy-lock.json
# Development tracking
.agent-work
# Claude Code hooks
.claude/

@ -17,3 +17,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This line was added by Claude!
Testing hooks - this should trigger pre and post hooks!
Hooks are now properly installed!

@ -9,5 +9,6 @@ return {
dependencies = {
'nvim-telescope/telescope.nvim', -- For agent picker
'tpope/vim-fugitive', -- Already installed, for diffs
'sindrets/diffview.nvim', -- For advanced diff viewing
},
}

@ -0,0 +1,186 @@
-- Diff review system for nvim-claude using diffview.nvim
local M = {}
-- State tracking
M.current_review = nil
function M.setup()
-- Set up keybindings
M.setup_keybindings()
vim.notify('Diff review system loaded (using diffview.nvim)', vim.log.levels.DEBUG)
end
-- Handle Claude edit completion
function M.handle_claude_edit(stash_ref, pre_edit_ref)
if not stash_ref then
vim.notify('No stash reference provided for diff review', vim.log.levels.ERROR)
return
end
vim.notify('Processing Claude edit with stash: ' .. stash_ref, vim.log.levels.INFO)
-- Get list of changed files
local changed_files = M.get_changed_files(stash_ref)
if not changed_files or #changed_files == 0 then
vim.notify('No changes detected from Claude edit', vim.log.levels.INFO)
return
end
-- Initialize review session
M.current_review = {
stash_ref = stash_ref,
pre_edit_ref = pre_edit_ref, -- Store the pre-edit commit reference
timestamp = os.time(),
changed_files = changed_files,
}
-- Notify user about changes
vim.notify(string.format(
'Claude made changes to %d file(s): %s',
#changed_files,
table.concat(changed_files, ', ')
), vim.log.levels.INFO)
vim.notify('Use <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
-- Get list of files changed in the stash
function M.get_changed_files(stash_ref)
local utils = require('nvim-claude.utils')
local cmd = string.format('git stash show %s --name-only', stash_ref)
local result = utils.exec(cmd)
if not result or result == '' then
return {}
end
local files = {}
for line in result:gmatch('[^\n]+') do
if line ~= '' then
table.insert(files, line)
end
end
return files
end
-- Set up keybindings for diff review
function M.setup_keybindings()
-- Review actions
vim.keymap.set('n', '<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' })
end
-- Open diffview for current review
function M.open_diffview()
if not M.current_review then
vim.notify('No active review session', vim.log.levels.INFO)
return
end
-- Check if diffview is available
local ok, diffview = pcall(require, 'diffview')
if not ok then
vim.notify('diffview.nvim not available, falling back to fugitive', vim.log.levels.WARN)
M.open_fugitive()
return
end
-- Use the pre-edit reference if available
if M.current_review.pre_edit_ref then
local cmd = 'DiffviewOpen ' .. M.current_review.pre_edit_ref
vim.notify('Opening diffview with pre-edit commit: ' .. cmd, vim.log.levels.INFO)
vim.cmd(cmd)
else
-- Fallback to comparing stash with its parent
vim.notify('No pre-edit commit found, falling back to stash comparison', vim.log.levels.WARN)
local cmd = string.format('DiffviewOpen %s^..%s', M.current_review.stash_ref, M.current_review.stash_ref)
vim.notify('Opening diffview: ' .. cmd, vim.log.levels.INFO)
vim.cmd(cmd)
end
end
-- Open fugitive diff (fallback)
function M.open_fugitive()
if not M.current_review then
vim.notify('No active review session', vim.log.levels.INFO)
return
end
-- Use fugitive to show diff
local cmd = 'Gdiffsplit ' .. M.current_review.stash_ref
vim.notify('Opening fugitive: ' .. cmd, vim.log.levels.INFO)
vim.cmd(cmd)
end
-- List changed files
function M.list_changes()
if not M.current_review then
vim.notify('No active review session', vim.log.levels.INFO)
return
end
local files = M.current_review.changed_files
if #files == 0 then
vim.notify('No changes found', vim.log.levels.INFO)
return
end
-- Create a telescope picker if available, otherwise just notify
local ok, telescope = pcall(require, 'telescope.pickers')
if ok then
M.telescope_changed_files()
else
vim.notify('Changed files:', vim.log.levels.INFO)
for i, file in ipairs(files) do
vim.notify(string.format(' %d. %s', i, file), vim.log.levels.INFO)
end
end
end
-- Telescope picker for changed files
function M.telescope_changed_files()
local pickers = require('telescope.pickers')
local finders = require('telescope.finders')
local conf = require('telescope.config').values
pickers.new({}, {
prompt_title = 'Claude Changed Files',
finder = finders.new_table({
results = M.current_review.changed_files,
}),
sorter = conf.generic_sorter({}),
attach_mappings = function(_, map)
map('i', '<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
return M

@ -0,0 +1,292 @@
-- Claude Code hooks integration for nvim-claude
local M = {}
-- Track hook state
M.pre_edit_commit = nil
function M.setup()
vim.notify('Hooks module loaded', vim.log.levels.DEBUG)
end
-- Pre-tool-use hook: Create a commit snapshot of the current state
function M.pre_tool_use_hook()
local utils = require('nvim-claude.utils')
-- Check if we're in a git repository
local git_root = utils.get_project_root()
if not git_root then
vim.notify('Not in a git repository', vim.log.levels.WARN)
return
end
-- Create timestamp for snapshot
local timestamp = os.time()
local commit_msg = string.format('claude-pre-edit-%d', timestamp)
-- Stage all current changes (including untracked files)
local add_cmd = string.format('cd "%s" && git add -A', git_root)
local add_result, add_err = utils.exec(add_cmd)
if add_err then
vim.notify('Failed to stage changes: ' .. add_err, vim.log.levels.ERROR)
return
end
-- Create a temporary commit
local commit_cmd = string.format('cd "%s" && git commit -m "%s" --allow-empty', git_root, commit_msg)
local commit_result, commit_err = utils.exec(commit_cmd)
if commit_err and not commit_err:match('nothing to commit') then
vim.notify('Failed to create snapshot commit: ' .. commit_err, vim.log.levels.ERROR)
return
end
-- Store the commit reference
M.pre_edit_commit = 'HEAD'
-- Also store in temp file for diff review to access
local temp_file = '/tmp/claude-pre-edit-commit'
utils.write_file(temp_file, 'HEAD')
vim.notify('Pre-Claude snapshot created: ' .. commit_msg, vim.log.levels.INFO)
end
-- Post-tool-use hook: Create stash of Claude's changes and trigger diff review
function M.post_tool_use_hook()
vim.notify('Post-tool-use hook triggered', vim.log.levels.INFO)
-- Use vim.schedule to ensure we're in the main thread
vim.schedule(function()
local utils = require('nvim-claude.utils')
-- Refresh all buffers to show Claude's changes
vim.cmd('checktime')
-- Check if Claude made any changes
local git_root = utils.get_project_root()
if not git_root then
vim.notify('Not in a git repository', vim.log.levels.WARN)
return
end
local status_cmd = string.format('cd "%s" && git status --porcelain', git_root)
local status_result = utils.exec(status_cmd)
if not status_result or status_result == '' then
vim.notify('No changes detected from Claude', vim.log.levels.INFO)
return
end
-- Create a stash of Claude's changes (but keep them in working directory)
local timestamp = os.date('%Y-%m-%d %H:%M:%S')
local stash_msg = string.format('[claude-edit] %s', timestamp)
-- Use git stash create to create stash without removing changes
local stash_cmd = string.format('cd "%s" && git stash create -u', git_root)
local stash_hash, stash_err = utils.exec(stash_cmd)
if not stash_err and stash_hash and stash_hash ~= '' then
-- Store the stash with a message
stash_hash = stash_hash:gsub('%s+', '') -- trim whitespace
local store_cmd = string.format('cd "%s" && git stash store -m "%s" %s', git_root, stash_msg, stash_hash)
utils.exec(store_cmd)
-- Get the pre-edit commit reference
local pre_edit_ref = utils.read_file('/tmp/claude-pre-edit-commit')
if pre_edit_ref then
pre_edit_ref = pre_edit_ref:gsub('%s+', '') -- trim whitespace
end
-- Trigger diff review
local ok, diff_review = pcall(require, 'nvim-claude.diff-review')
if ok then
diff_review.handle_claude_edit('stash@{0}', pre_edit_ref)
else
vim.notify('Diff review module not available: ' .. tostring(diff_review), vim.log.levels.ERROR)
end
else
vim.notify('Failed to create stash of Claude changes', vim.log.levels.ERROR)
end
end)
end
-- Manual hook testing
function M.test_hooks()
vim.notify('=== Testing nvim-claude hooks ===', vim.log.levels.INFO)
-- Test pre-tool-use hook
vim.notify('1. Testing pre-tool-use hook (creating snapshot)...', vim.log.levels.INFO)
M.pre_tool_use_hook()
-- Simulate making a change
vim.notify('2. Make some changes to test files now...', vim.log.levels.INFO)
-- Test post-tool-use hook after a delay
vim.notify('3. Will trigger post-tool-use hook in 3 seconds...', vim.log.levels.INFO)
vim.defer_fn(function()
M.post_tool_use_hook()
end, 3000)
vim.notify('=== Hook testing started - make changes now! ===', vim.log.levels.INFO)
end
-- Install Claude Code hooks
function M.install_hooks()
local utils = require('nvim-claude.utils')
-- Get project root
local project_root = utils.get_project_root()
if not project_root then
vim.notify('Not in a git repository', vim.log.levels.ERROR)
return
end
-- Create .claude directory
local claude_dir = project_root .. '/.claude'
if not vim.fn.isdirectory(claude_dir) then
vim.fn.mkdir(claude_dir, 'p')
end
-- Create hooks configuration
local server_name = vim.v.servername or 'NVIM'
local pre_command = string.format('nvim --headless --server %s --remote-send "<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 = ".*", -- Match all tools
hooks = {
{
type = "command",
command = pre_command
}
}
}
},
PostToolUse = {
{
matcher = ".*", -- Match all tools
hooks = {
{
type = "command",
command = post_command
}
}
}
}
}
}
-- Write hooks configuration
local settings_file = claude_dir .. '/settings.json'
local success, err = utils.write_json(settings_file, hooks_config)
if success then
-- Add .claude to gitignore if needed
local gitignore_path = project_root .. '/.gitignore'
local gitignore_content = utils.read_file(gitignore_path) or ''
if not gitignore_content:match('%.claude/') then
local new_content = gitignore_content .. '\n# Claude Code hooks\n.claude/\n'
utils.write_file(gitignore_path, new_content)
vim.notify('Added .claude/ to .gitignore', vim.log.levels.INFO)
end
vim.notify('Claude Code hooks installed successfully', vim.log.levels.INFO)
vim.notify('Hooks configuration written to: ' .. settings_file, vim.log.levels.INFO)
else
vim.notify('Failed to install hooks: ' .. (err or 'unknown error'), vim.log.levels.ERROR)
end
end
-- Uninstall Claude Code hooks
function M.uninstall_hooks()
local utils = require('nvim-claude.utils')
-- Get project root
local project_root = utils.get_project_root()
if not project_root then
vim.notify('Not in a git repository', vim.log.levels.ERROR)
return
end
local settings_file = project_root .. '/.claude/settings.json'
if vim.fn.filereadable(settings_file) then
vim.fn.delete(settings_file)
vim.notify('Claude Code hooks uninstalled', vim.log.levels.INFO)
else
vim.notify('No hooks configuration found', vim.log.levels.INFO)
end
end
-- Commands for manual hook management
function M.setup_commands()
vim.api.nvim_create_user_command('ClaudeTestHooks', function()
M.test_hooks()
end, {
desc = 'Test Claude Code hooks'
})
vim.api.nvim_create_user_command('ClaudeTestDiff', function()
local utils = require('nvim-claude.utils')
-- Check if we're in a git repository
local git_root = utils.get_project_root()
if not git_root then
vim.notify('Not in a git repository', vim.log.levels.WARN)
return
end
-- Check if there are any changes
local status_cmd = string.format('cd "%s" && git status --porcelain', git_root)
local status_result = utils.exec(status_cmd)
if not status_result or status_result == '' then
vim.notify('No changes to test', vim.log.levels.INFO)
return
end
-- Create test stash without restoring (to avoid conflicts)
local timestamp = os.date('%Y-%m-%d %H:%M:%S')
local stash_msg = string.format('[claude-test] %s', timestamp)
local stash_cmd = string.format('cd "%s" && git stash push -u -m "%s"', git_root, stash_msg)
local stash_result, stash_err = utils.exec(stash_cmd)
if stash_err then
vim.notify('Failed to create test stash: ' .. stash_err, vim.log.levels.ERROR)
return
end
-- Trigger diff review with the stash (no pre-edit ref for manual test)
local diff_review = require('nvim-claude.diff-review')
diff_review.handle_claude_edit('stash@{0}', nil)
-- Pop the stash to restore changes
vim.defer_fn(function()
local pop_cmd = string.format('cd "%s" && git stash pop --quiet', git_root)
utils.exec(pop_cmd)
vim.cmd('checktime') -- Refresh buffers
end, 100)
end, {
desc = 'Test Claude diff review with current changes'
})
vim.api.nvim_create_user_command('ClaudeInstallHooks', function()
M.install_hooks()
end, {
desc = 'Install Claude Code hooks for this project'
})
vim.api.nvim_create_user_command('ClaudeUninstallHooks', function()
M.uninstall_hooks()
end, {
desc = 'Uninstall Claude Code hooks for this project'
})
end
return M

@ -115,14 +115,19 @@ function M.setup(user_config)
M.utils = require('nvim-claude.utils')
M.commands = require('nvim-claude.commands')
M.registry = require('nvim-claude.registry')
M.hooks = require('nvim-claude.hooks')
M.diff_review = require('nvim-claude.diff-review')
-- Initialize submodules with config
M.tmux.setup(M.config.tmux)
M.git.setup(M.config.agents)
M.registry.setup(M.config.agents)
M.hooks.setup()
M.diff_review.setup()
-- Set up commands
M.commands.setup(M)
M.hooks.setup_commands()
-- Set up keymappings if enabled
if M.config.mappings then

@ -113,4 +113,38 @@ function M.tmux_supports_length_percent()
return M.tmux_version() >= 3.4
end
-- Write JSON to file
function M.write_json(path, data)
local success, json = pcall(vim.fn.json_encode, data)
if not success then
return false, 'Failed to encode JSON: ' .. json
end
local file = io.open(path, 'w')
if not file then
return false, 'Failed to open file for writing: ' .. path
end
-- Pretty print JSON
local formatted = json:gsub('},{', '},\n {'):gsub('\\{', '{\n '):gsub('\\}', '\n}')
file:write(formatted)
file:close()
return true, nil
end
-- Read JSON from file
function M.read_json(path)
local content = M.read_file(path)
if not content then
return nil, 'Failed to read file: ' .. path
end
local success, data = pcall(vim.fn.json_decode, content)
if not success then
return nil, 'Failed to decode JSON: ' .. data
end
return data, nil
end
return M
Loading…
Cancel
Save