irongit

My nvim configuration

feat: visual mode claude

huncholanehuncholaneauthored
parent 2b285e7commit 28d17bbd18080636d61cd299b5296e958afe0ceeBrowse files

3 files changed, +178 -19

+171-0claude-nvim/lua/claude/edit.lua
@@ -0,0 +1,171 @@
1+local M = {}
2+
3+local spinner_frames = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
4+local ns = vim.api.nvim_create_namespace("claude_edit")
5+local skip_permissions = false
6+
7+function M.setup(opts)
8+ if opts and opts.skip_permissions then
9+ skip_permissions = true
10+ end
11+end
12+
13+local function strip_fences(text)
14+ local lines = vim.split(text, "\n", { plain = true })
15+ while #lines > 0 and lines[#lines] == "" do
16+ table.remove(lines)
17+ end
18+ if #lines >= 2 and lines[1]:match("^```") then
19+ table.remove(lines, 1)
20+ if lines[#lines] and lines[#lines]:match("^```%s*$") then
21+ table.remove(lines)
22+ end
23+ end
24+ return lines
25+end
26+
27+local function start_spinner(buf, row)
28+ local idx = 1
29+ local function virt(i)
30+ return { { " " .. spinner_frames[i] .. " claude…", "Comment" } }
31+ end
32+ local ok, mark = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, {
33+ virt_text = virt(idx),
34+ virt_text_pos = "eol",
35+ hl_mode = "combine",
36+ })
37+ if not ok then return function() end end
38+
39+ local timer = vim.uv.new_timer()
40+ timer:start(80, 80, vim.schedule_wrap(function()
41+ if not vim.api.nvim_buf_is_valid(buf) then
42+ timer:stop(); timer:close()
43+ return
44+ end
45+ idx = idx % #spinner_frames + 1
46+ pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, {
47+ id = mark,
48+ virt_text = virt(idx),
49+ virt_text_pos = "eol",
50+ hl_mode = "combine",
51+ })
52+ end))
53+
54+ return function()
55+ if not timer:is_closing() then
56+ timer:stop(); timer:close()
57+ end
58+ pcall(vim.api.nvim_buf_del_extmark, buf, ns, mark)
59+ end
60+end
61+
62+local function run_claude(prompt, selection, on_done)
63+ local full = string.format(
64+ "Apply the following instruction to the code below. Output ONLY the resulting replacement code. No explanation, no commentary, no markdown code fences.\n\nInstruction:\n%s\n\nCode:\n%s",
65+ prompt, selection
66+ )
67+ local cmd = { "claude", "-p", full }
68+ if skip_permissions then
69+ table.insert(cmd, "--dangerously-skip-permissions")
70+ end
71+ vim.system(cmd, { text = true }, vim.schedule_wrap(function(out)
72+ if out.code ~= 0 then
73+ on_done(nil, (out.stderr ~= "" and out.stderr) or ("claude exited " .. tostring(out.code)))
74+ else
75+ on_done(out.stdout or "", nil)
76+ end
77+ end))
78+end
79+
80+function M.prompt_visual()
81+ -- Capture selection text + range
82+ local old_reg = vim.fn.getreg('"')
83+ local old_regtype = vim.fn.getregtype('"')
84+ vim.cmd('noautocmd normal! "vy')
85+ local selection = vim.fn.getreg("v")
86+ vim.fn.setreg('"', old_reg, old_regtype)
87+
88+ local s = vim.fn.getpos("'<")
89+ local e = vim.fn.getpos("'>")
90+ local target_buf = vim.api.nvim_get_current_buf()
91+ local start_row = s[2] - 1
92+ local end_row = e[2] - 1
93+
94+ if selection == nil or selection == "" then
95+ vim.notify("claude: empty selection", vim.log.levels.WARN)
96+ return
97+ end
98+
99+ -- Floating popup
100+ local pop_buf = vim.api.nvim_create_buf(false, true)
101+ vim.bo[pop_buf].bufhidden = "wipe"
102+ vim.bo[pop_buf].filetype = "markdown"
103+
104+ local width = math.min(80, math.max(40, math.floor(vim.o.columns * 0.6)))
105+ local height = 8
106+ local pop_win = vim.api.nvim_open_win(pop_buf, true, {
107+ relative = "editor",
108+ row = math.floor((vim.o.lines - height) / 2),
109+ col = math.floor((vim.o.columns - width) / 2),
110+ width = width,
111+ height = height,
112+ style = "minimal",
113+ border = "rounded",
114+ title = " Claude prompt — <C-s> apply <C-c> cancel ",
115+ title_pos = "center",
116+ })
117+
118+ vim.cmd("startinsert")
119+
120+ local closed = false
121+ local function close_popup()
122+ if closed then return end
123+ closed = true
124+ if vim.api.nvim_win_is_valid(pop_win) then
125+ vim.api.nvim_win_close(pop_win, true)
126+ end
127+ end
128+
129+ local function apply()
130+ if closed then return end
131+ local lines = vim.api.nvim_buf_get_lines(pop_buf, 0, -1, false)
132+ local prompt = table.concat(lines, "\n")
133+ close_popup()
134+ if prompt:match("^%s*$") then
135+ vim.notify("claude: empty prompt", vim.log.levels.WARN)
136+ return
137+ end
138+
139+ if not vim.api.nvim_buf_is_valid(target_buf) then return end
140+ local stop_spin = start_spinner(target_buf, start_row)
141+
142+ run_claude(prompt, selection, function(stdout, err)
143+ stop_spin()
144+ if err then
145+ vim.notify("claude: " .. err, vim.log.levels.ERROR)
146+ return
147+ end
148+ if not vim.api.nvim_buf_is_valid(target_buf) then return end
149+ local out_lines = strip_fences(stdout or "")
150+ if #out_lines == 0 then
151+ vim.notify("claude: empty response", vim.log.levels.WARN)
152+ return
153+ end
154+ vim.api.nvim_buf_set_lines(target_buf, start_row, end_row + 1, false, out_lines)
155+ end)
156+ end
157+
158+ local km = { buffer = pop_buf, nowait = true, silent = true }
159+ vim.keymap.set({ "n", "i" }, "<C-s>", apply, km)
160+ vim.keymap.set({ "n", "i" }, "<C-c>", close_popup, km)
161+ vim.keymap.set("n", "q", close_popup, km)
162+ vim.keymap.set("n", "<Esc>", close_popup, km)
163+
164+ vim.api.nvim_create_autocmd("BufLeave", {
165+ buffer = pop_buf,
166+ once = true,
167+ callback = close_popup,
168+ })
169+end
170+
171+return M
+6-19claude-nvim/lua/claude/init.lua
@@ -2,6 +2,7 @@ local M = {}
22
33 local terminal = require("claude.terminal")
44 local session = require("claude.session")
5+local edit = require("claude.edit")
56
67 ---@class ClaudeConfig
78 local defaults = {
@@ -19,31 +20,17 @@ function M.setup(opts)
1920 opts = vim.tbl_deep_extend("force", defaults, opts or {})
2021 terminal.setup(opts)
2122 session.setup(opts)
23+ edit.setup(opts)
2224
2325 vim.keymap.set("n", "<leader>c", "", { desc = "Claude" })
2426
2527 -- Toggle claude panel (resumes last session)
2628 vim.keymap.set("n", "<leader>ct", session.continue, { desc = "Claude: Toggle panel" })
2729
28- -- Send visual selection to claude
29- vim.keymap.set("v", "<leader>cs", function()
30- -- Yank selection into register
31- local old_reg = vim.fn.getreg('"')
32- local old_regtype = vim.fn.getregtype('"')
33- vim.cmd('noautocmd normal! "vy')
34- local selection = vim.fn.getreg("v")
35- vim.fn.setreg('"', old_reg, old_regtype)
36-
37- if not terminal.is_open() then
38- session.continue()
39- -- Wait for terminal to be ready
40- vim.defer_fn(function()
41- terminal.send(selection .. "\n")
42- end, 500)
43- else
44- terminal.send(selection .. "\n")
45- end
46- end, { desc = "Claude: Send selection" })
30+ -- Visual <leader>c → prompt popup that edits selection in place via claude -p
31+ vim.keymap.set("v", "<leader>c", function()
32+ edit.prompt_visual()
33+ end, { desc = "Claude: Edit selection with prompt" })
4734
4835 -- Send current buffer to claude
4936 vim.keymap.set("n", "<leader>cb", function()
+1-0lua/keymaps.lua
@@ -118,6 +118,7 @@ easymap("n", "g;", "m'A;<esc>`'", "Append Colon")
118118 easymap("n", "g,", "m'A,<esc>`'", "Append Comma")
119119 easymap("n", "gl", "", "LSP")
120120 easymap("n", "glt", ":ToggleDiagnostics<cr>", "Toggle")
121+vim.keymap.set("n", "gc", require("telescope.builtin").grep_string, { desc= "Telescope word"})
121122
122123 -- ########################################################################## --
123124 -- Surroundings