My nvim configuration
feat(claude): add AI-generated commit message command
Add `<leader>cg` mapping that pipes the staged diff to `claude -p`, strips markdown fences, and commits with the returned message.
2 files changed, +71 -0
+68-0claude-nvim/lua/claude/commit.lua
| @@ -0,0 +1,68 @@ | ||
| 1 | +local M = {} | |
| 2 | + | |
| 3 | +local skip_permissions = false | |
| 4 | + | |
| 5 | +function M.setup(opts) | |
| 6 | + if opts and opts.skip_permissions then skip_permissions = true end | |
| 7 | +end | |
| 8 | + | |
| 9 | +local function strip_fences(text) | |
| 10 | + local lines = vim.split(text or "", "\n", { plain = true }) | |
| 11 | + while #lines > 0 and lines[#lines] == "" do table.remove(lines) end | |
| 12 | + if #lines >= 2 and lines[1]:match("^```") then | |
| 13 | + table.remove(lines, 1) | |
| 14 | + if lines[#lines] and lines[#lines]:match("^```%s*$") then | |
| 15 | + table.remove(lines) | |
| 16 | + end | |
| 17 | + end | |
| 18 | + return table.concat(lines, "\n") | |
| 19 | +end | |
| 20 | + | |
| 21 | +function M.commit() | |
| 22 | + vim.system({ "git", "diff", "--staged" }, { text = true }, vim.schedule_wrap(function(diff_out) | |
| 23 | + if diff_out.code ~= 0 then | |
| 24 | + vim.notify("git diff failed: " .. (diff_out.stderr or ""), vim.log.levels.ERROR) | |
| 25 | + return | |
| 26 | + end | |
| 27 | + local diff = diff_out.stdout or "" | |
| 28 | + if diff:match("^%s*$") then | |
| 29 | + vim.notify("claude: no staged changes", vim.log.levels.WARN) | |
| 30 | + return | |
| 31 | + end | |
| 32 | + vim.notify("claude: writing commit message…") | |
| 33 | + local prompt = "Write a Conventional Commits style commit message for the following staged git diff. " | |
| 34 | + .. "Subject line under 72 chars, imperative mood. Optional body explains why if non-obvious. " | |
| 35 | + .. "Output ONLY the commit message. No explanation, no markdown fences.\n\n" | |
| 36 | + .. diff | |
| 37 | + local cmd = { "claude", "-p", prompt } | |
| 38 | + if skip_permissions then table.insert(cmd, "--dangerously-skip-permissions") end | |
| 39 | + vim.system(cmd, { text = true }, vim.schedule_wrap(function(out) | |
| 40 | + if out.code ~= 0 then | |
| 41 | + vim.notify("claude failed: " .. (out.stderr or ""), vim.log.levels.ERROR) | |
| 42 | + return | |
| 43 | + end | |
| 44 | + local msg = strip_fences(out.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") | |
| 45 | + if msg == "" then | |
| 46 | + vim.notify("claude: empty commit message", vim.log.levels.ERROR) | |
| 47 | + return | |
| 48 | + end | |
| 49 | + vim.system( | |
| 50 | + { "git", "commit", "-F", "-" }, | |
| 51 | + { text = true, stdin = msg }, | |
| 52 | + vim.schedule_wrap(function(commit_out) | |
| 53 | + if commit_out.code ~= 0 then | |
| 54 | + vim.notify( | |
| 55 | + "git commit failed: " .. (commit_out.stderr ~= "" and commit_out.stderr or commit_out.stdout or ""), | |
| 56 | + vim.log.levels.ERROR | |
| 57 | + ) | |
| 58 | + return | |
| 59 | + end | |
| 60 | + local subject = msg:match("^[^\n]*") or msg | |
| 61 | + vim.notify("commit: " .. subject) | |
| 62 | + end) | |
| 63 | + ) | |
| 64 | + end)) | |
| 65 | + end)) | |
| 66 | +end | |
| 67 | + | |
| 68 | +return M |
+3-0claude-nvim/lua/claude/init.lua
| @@ -6,6 +6,7 @@ local highlights = require("claude.highlights") | ||
| 6 | 6 | local pickers = require("claude.pickers") |
| 7 | 7 | local edit = require("claude.edit") |
| 8 | 8 | local timetravel = require("claude.timetravel") |
| 9 | +local commit = require("claude.commit") | |
| 9 | 10 | |
| 10 | 11 | ---@class ClaudeConfig |
| 11 | 12 | local defaults = { |
| @@ -29,6 +30,7 @@ function M.setup(opts) | ||
| 29 | 30 | runner.setup(opts) |
| 30 | 31 | highlights.setup(opts) |
| 31 | 32 | edit.setup(opts) |
| 33 | + commit.setup(opts) | |
| 32 | 34 | |
| 33 | 35 | vim.keymap.set("n", "<leader>c", "", { desc = "Claude" }) |
| 34 | 36 | |
| @@ -36,6 +38,7 @@ function M.setup(opts) | ||
| 36 | 38 | vim.keymap.set("n", "<leader>ci", edit.prompt_insert, { desc = "Claude: Insert at cursor with prompt" }) |
| 37 | 39 | vim.keymap.set("n", "<leader>cu", timetravel.undo, { desc = "Claude: Undo last claude change" }) |
| 38 | 40 | vim.keymap.set("n", "<leader>cU", timetravel.redo, { desc = "Claude: Redo claude change" }) |
| 41 | + vim.keymap.set("n", "<leader>cg", commit.commit, { desc = "Claude: Commit staged with generated message" }) | |
| 39 | 42 | vim.keymap.set("n", "<leader>ct", ui.toggle, { desc = "Claude: Toggle panel" }) |
| 40 | 43 | vim.keymap.set("n", "<leader>cn", ui.start_new, { desc = "Claude: New session" }) |
| 41 | 44 | vim.keymap.set("n", "<leader>cr", ui.resume_last, { desc = "Claude: Resume last session" }) |