My nvim configuration
feat(claude): insert similar to edit with ci
2 files changed, +113 -5
+112-5claude-nvim/lua/claude/edit.lua
| @@ -24,17 +24,18 @@ local function strip_fences(text) | ||
| 24 | 24 | return lines |
| 25 | 25 | end |
| 26 | 26 | |
| 27 | -local function start_spinner(buf, row) | |
| 27 | +local function start_spinner(buf, row, label) | |
| 28 | + label = label and label ~= "" and (" " .. label) or "" | |
| 28 | 29 | local idx = 1 |
| 29 | 30 | local function virt(i) |
| 30 | - return { { " " .. spinner_frames[i] .. " claude…", "Comment" } } | |
| 31 | + return { { " " .. spinner_frames[i] .. " claude…" .. label, "Comment" } } | |
| 31 | 32 | end |
| 32 | 33 | local ok, mark = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, { |
| 33 | 34 | virt_text = virt(idx), |
| 34 | 35 | virt_text_pos = "eol", |
| 35 | 36 | hl_mode = "combine", |
| 36 | 37 | }) |
| 37 | - if not ok then return function() end end | |
| 38 | + if not ok then return function() end, nil end | |
| 38 | 39 | |
| 39 | 40 | local timer = vim.uv.new_timer() |
| 40 | 41 | timer:start(80, 80, vim.schedule_wrap(function() |
| @@ -51,12 +52,13 @@ local function start_spinner(buf, row) | ||
| 51 | 52 | }) |
| 52 | 53 | end)) |
| 53 | 54 | |
| 54 | - return function() | |
| 55 | + local stop = function() | |
| 55 | 56 | if not timer:is_closing() then |
| 56 | 57 | timer:stop(); timer:close() |
| 57 | 58 | end |
| 58 | 59 | pcall(vim.api.nvim_buf_del_extmark, buf, ns, mark) |
| 59 | 60 | end |
| 61 | + return stop, mark | |
| 60 | 62 | end |
| 61 | 63 | |
| 62 | 64 | local function run_claude(prompt, selection, on_done) |
| @@ -138,7 +140,8 @@ function M.prompt_visual() | ||
| 138 | 140 | end |
| 139 | 141 | |
| 140 | 142 | if not vim.api.nvim_buf_is_valid(target_buf) then return end |
| 141 | - local stop_spin = start_spinner(target_buf, start_row) | |
| 143 | + local label = (prompt:gsub("%s+", " ")):sub(1, 60) | |
| 144 | + local stop_spin = start_spinner(target_buf, start_row, label) | |
| 142 | 145 | |
| 143 | 146 | run_claude(prompt, selection, function(stdout, err) |
| 144 | 147 | stop_spin() |
| @@ -169,4 +172,108 @@ function M.prompt_visual() | ||
| 169 | 172 | }) |
| 170 | 173 | end |
| 171 | 174 | |
| 175 | +local function run_claude_insert(prompt, on_done) | |
| 176 | + local full = string.format( | |
| 177 | + "Generate code for the following instruction. Output ONLY the code to be inserted, with no explanation, no commentary, no markdown code fences.\n\nInstruction:\n%s", | |
| 178 | + prompt | |
| 179 | + ) | |
| 180 | + local cmd = { "claude", "-p", full } | |
| 181 | + if skip_permissions then | |
| 182 | + table.insert(cmd, "--dangerously-skip-permissions") | |
| 183 | + end | |
| 184 | + vim.system(cmd, { text = true }, vim.schedule_wrap(function(out) | |
| 185 | + if out.code ~= 0 then | |
| 186 | + on_done(nil, (out.stderr ~= "" and out.stderr) or ("claude exited " .. tostring(out.code))) | |
| 187 | + else | |
| 188 | + on_done(out.stdout or "", nil) | |
| 189 | + end | |
| 190 | + end)) | |
| 191 | +end | |
| 192 | + | |
| 193 | +function M.prompt_insert() | |
| 194 | + local target_buf = vim.api.nvim_get_current_buf() | |
| 195 | + local cur = vim.api.nvim_win_get_cursor(0) | |
| 196 | + local row = cur[1] - 1 | |
| 197 | + | |
| 198 | + local pop_buf = vim.api.nvim_create_buf(false, true) | |
| 199 | + vim.bo[pop_buf].bufhidden = "wipe" | |
| 200 | + vim.bo[pop_buf].filetype = "markdown" | |
| 201 | + | |
| 202 | + local width = math.min(80, math.max(40, math.floor(vim.o.columns * 0.6))) | |
| 203 | + local height = 8 | |
| 204 | + local pop_win = vim.api.nvim_open_win(pop_buf, true, { | |
| 205 | + relative = "editor", | |
| 206 | + row = math.floor((vim.o.lines - height) / 2), | |
| 207 | + col = math.floor((vim.o.columns - width) / 2), | |
| 208 | + width = width, | |
| 209 | + height = height, | |
| 210 | + style = "minimal", | |
| 211 | + border = "rounded", | |
| 212 | + title = " Claude insert — <C-s> apply <C-c> cancel ", | |
| 213 | + title_pos = "center", | |
| 214 | + }) | |
| 215 | + | |
| 216 | + vim.cmd("startinsert") | |
| 217 | + | |
| 218 | + local closed = false | |
| 219 | + local function close_popup() | |
| 220 | + if closed then return end | |
| 221 | + closed = true | |
| 222 | + pcall(vim.cmd, "stopinsert") | |
| 223 | + if vim.api.nvim_win_is_valid(pop_win) then | |
| 224 | + vim.api.nvim_win_close(pop_win, true) | |
| 225 | + end | |
| 226 | + end | |
| 227 | + | |
| 228 | + local function apply() | |
| 229 | + if closed then return end | |
| 230 | + local lines = vim.api.nvim_buf_get_lines(pop_buf, 0, -1, false) | |
| 231 | + local prompt = table.concat(lines, "\n") | |
| 232 | + close_popup() | |
| 233 | + if prompt:match("^%s*$") then | |
| 234 | + vim.notify("claude: empty prompt", vim.log.levels.WARN) | |
| 235 | + return | |
| 236 | + end | |
| 237 | + | |
| 238 | + if not vim.api.nvim_buf_is_valid(target_buf) then return end | |
| 239 | + | |
| 240 | + -- Place a tracking extmark at the insertion row so position survives upstream edits | |
| 241 | + local anchor = vim.api.nvim_buf_set_extmark(target_buf, ns, row, 0, {}) | |
| 242 | + local label = (prompt:gsub("%s+", " ")):sub(1, 60) | |
| 243 | + local stop_spin = start_spinner(target_buf, row, label) | |
| 244 | + | |
| 245 | + run_claude_insert(prompt, function(stdout, err) | |
| 246 | + stop_spin() | |
| 247 | + if err then | |
| 248 | + pcall(vim.api.nvim_buf_del_extmark, target_buf, ns, anchor) | |
| 249 | + vim.notify("claude: " .. err, vim.log.levels.ERROR) | |
| 250 | + return | |
| 251 | + end | |
| 252 | + if not vim.api.nvim_buf_is_valid(target_buf) then return end | |
| 253 | + local out_lines = strip_fences(stdout or "") | |
| 254 | + if #out_lines == 0 then | |
| 255 | + pcall(vim.api.nvim_buf_del_extmark, target_buf, ns, anchor) | |
| 256 | + vim.notify("claude: empty response", vim.log.levels.WARN) | |
| 257 | + return | |
| 258 | + end | |
| 259 | + local pos = vim.api.nvim_buf_get_extmark_by_id(target_buf, ns, anchor, {}) | |
| 260 | + pcall(vim.api.nvim_buf_del_extmark, target_buf, ns, anchor) | |
| 261 | + local insert_at = (pos and pos[1] or row) + 1 | |
| 262 | + pcall(vim.api.nvim_buf_set_lines, target_buf, insert_at, insert_at, false, out_lines) | |
| 263 | + end) | |
| 264 | + end | |
| 265 | + | |
| 266 | + local km = { buffer = pop_buf, nowait = true, silent = true } | |
| 267 | + vim.keymap.set({ "n", "i" }, "<C-s>", apply, km) | |
| 268 | + vim.keymap.set({ "n", "i" }, "<C-c>", close_popup, km) | |
| 269 | + vim.keymap.set("n", "q", close_popup, km) | |
| 270 | + vim.keymap.set("n", "<Esc>", close_popup, km) | |
| 271 | + | |
| 272 | + vim.api.nvim_create_autocmd("BufLeave", { | |
| 273 | + buffer = pop_buf, | |
| 274 | + once = true, | |
| 275 | + callback = close_popup, | |
| 276 | + }) | |
| 277 | +end | |
| 278 | + | |
| 172 | 279 | return M |
+1-0claude-nvim/lua/claude/init.lua
| @@ -32,6 +32,7 @@ function M.setup(opts) | ||
| 32 | 32 | vim.keymap.set("n", "<leader>c", "", { desc = "Claude" }) |
| 33 | 33 | |
| 34 | 34 | vim.keymap.set("n", "<leader>cc", ui.cancel_current, { desc = "Claude: Cancel current prompt" }) |
| 35 | + vim.keymap.set("n", "<leader>ci", edit.prompt_insert, { desc = "Claude: Insert at cursor with prompt" }) | |
| 35 | 36 | vim.keymap.set("n", "<leader>ct", ui.toggle, { desc = "Claude: Toggle panel" }) |
| 36 | 37 | vim.keymap.set("n", "<leader>cn", ui.start_new, { desc = "Claude: New session" }) |
| 37 | 38 | vim.keymap.set("n", "<leader>cr", ui.resume_last, { desc = "Claude: Resume last session" }) |