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