Claude CodeNeovimLua工作流LSP

Neovim + Claude Code:Lua 生态深度集成

把 Claude Code 接入现代 Neovim 工作流,用 Lua 自定义 keymap、Telescope 跳转、toggleterm 浮窗、diffview 审改,附完整 init.lua 片段。

· 阅读约 20 分钟

Claude Code 已经能跑了,但如果你只是在 :terminalclaude 一下,等于把一辆赛车当代步车开。Neovim 0.9 之后整个生态都迁到了 Lua,Telescope、toggleterm、diffview、which-key 这些插件本身就是为「键盘流 + 可编程」设计的,跟 Claude Code 是天作之合。本文不讲 Neovim 怎么装,只讲已经装好 Neovim 和 Claude Code 的人怎么把两者揉在一起。


Neovim 0.9+ 生态速览(vim 重度用户友好版)

如果你来自 vim 8.x,下面这些概念是新东西,先对齐一下:

概念旧 Vim现代 Neovim
配置语言vimscriptLua(vimscript 仍兼容)
包管理器Vundle / vim-pluglazy.nvim / packer.nvim
补全 / 跳转YouCompleteMe、coc.nvim内置 LSP + nvim-cmp
语法高亮regexTree-sitter(AST 级)
模糊查找fzf.vimTelescope.nvim
终端:terminal 单开toggleterm.nvim 浮窗
Diff 查看:diffsplitdiffview.nvim 三栏

关键变化是「Lua 函数可以被绑到 keymap 上」,这给了 Claude Code 集成一个绝佳挂载点——任何想做的事都可以包成一个 Lua 函数,再 vim.keymap.set 绑一下完事。


把 Claude Code 当 Neovim 助手:选中代码问 Claude

最常用的场景:选中一段代码,按一下快捷键,让 Claude Code 解释这段代码,结果出现在新 buffer 里。

-- ~/.config/nvim/lua/plugins/claude.lua
local M = {}

-- 获取当前 visual 选区内容
local function get_visual_selection()
  local s_start = vim.fn.getpos("'<")
  local s_end = vim.fn.getpos("'>")
  local n_lines = math.abs(s_end[2] - s_start[2]) + 1
  local lines = vim.api.nvim_buf_get_lines(0, s_start[2] - 1, s_end[2], false)
  if #lines == 0 then return "" end
  lines[1] = string.sub(lines[1], s_start[3], -1)
  if n_lines == 1 then
    lines[n_lines] = string.sub(lines[n_lines], 1, s_end[3] - s_start[3] + 1)
  else
    lines[n_lines] = string.sub(lines[n_lines], 1, s_end[3])
  end
  return table.concat(lines, "\n")
end

-- 把 Claude Code 的输出灌到新 buffer
function M.ask_claude(prompt_template)
  local code = get_visual_selection()
  if code == "" then
    vim.notify("No selection", vim.log.levels.WARN)
    return
  end
  local prompt = prompt_template .. "\n\n```\n" .. code .. "\n```"
  -- 用 -p 模式拿到完整输出
  local cmd = { "claude", "-p", prompt }
  vim.system(cmd, { text = true }, function(obj)
    vim.schedule(function()
      vim.cmd("vnew")
      vim.bo.buftype = "nofile"
      vim.bo.filetype = "markdown"
      local lines = vim.split(obj.stdout or "", "\n")
      vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
    end)
  end)
end

return M

然后绑快捷键:

-- ~/.config/nvim/lua/keymaps.lua
local claude = require("plugins.claude")

vim.keymap.set("v", "<leader>ce", function()
  claude.ask_claude("用中文解释这段代码做了什么,注意边界条件")
end, { desc = "Claude: 解释选中代码" })

vim.keymap.set("v", "<leader>cr", function()
  claude.ask_claude("重构这段代码,目标:更简洁、性能更好,给出修改后版本和理由")
end, { desc = "Claude: 重构选中代码" })

vim.keymap.set("v", "<leader>ct", function()
  claude.ask_claude("为这段代码写单测,框架自选最合适的")
end, { desc = "Claude: 为选中代码写测试" })

v 进入 visual mode,框出一段代码,再按 <leader>ce,右边就会冒出一个 markdown buffer 显示 Claude 的解释。vim.system 是异步的,所以等回答时编辑器不会卡住。


Telescope.nvim 配合:让 Claude 列文件,你来跳

Claude Code 经常会给你一串「相关文件」列表,比如「这个 bug 涉及 src/auth.ts:42src/middleware.ts:88tests/auth.test.ts:120」。手敲 :e 一个个开太累,把这个列表喂给 Telescope 就能边看边跳。

local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")

function M.pick_from_claude_output()
  -- 从 + 寄存器(系统剪贴板)读 Claude 输出
  local text = vim.fn.getreg("+")
  local files = {}
  -- 简单匹配 path:lineno 模式
  for path, line in text:gmatch("([%w%./_%-]+%.%w+):(%d+)") do
    table.insert(files, { path = path, line = tonumber(line) })
  end
  if #files == 0 then
    vim.notify("剪贴板里没找到文件路径", vim.log.levels.WARN)
    return
  end

  pickers.new({}, {
    prompt_title = "Claude 提到的文件",
    finder = finders.new_table({
      results = files,
      entry_maker = function(entry)
        return {
          value = entry,
          display = entry.path .. ":" .. entry.line,
          ordinal = entry.path,
        }
      end,
    }),
    sorter = conf.generic_sorter({}),
    attach_mappings = function(_, map)
      actions.select_default:replace(function(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        actions.close(prompt_bufnr)
        vim.cmd("edit +" .. selection.value.line .. " " .. selection.value.path)
      end)
      return true
    end,
  }):find()
end

vim.keymap.set("n", "<leader>cf", M.pick_from_claude_output,
  { desc = "Claude: 从输出挑文件跳转" })

工作流变成:在 Claude 终端里 Ctrl+Shift+C 复制全部输出 → 在 Neovim 里 <leader>cf → Telescope 弹窗 → 模糊搜索 / 上下选 → 回车跳到文件指定行。


mason.nvim、lspconfig 跟 Claude Code 协作

mason.nvim 管 LSP server 二进制,nvim-lspconfig 管 LSP 配置。你不知道某语言用哪个 LSP 时,可以直接问 Claude Code:

claude -p "我要给一个 Rust + WASM 项目配 Neovim LSP,建议哪些 mason 包?给出 mason-lspconfig 的 ensure_installed 配置"

Claude 会给类似:

require("mason").setup()
require("mason-lspconfig").setup({
  ensure_installed = {
    "rust_analyzer",        -- Rust
    "tsserver",             -- TypeScript(如果有前端)
    "tailwindcss",          -- 如果用 Tailwind
    "html", "cssls",
  },
})

require("lspconfig").rust_analyzer.setup({
  settings = {
    ["rust-analyzer"] = {
      cargo = { allFeatures = true, target = "wasm32-unknown-unknown" },
      check = { command = "clippy" },
    },
  },
})

把它存到 lua/plugins/lsp.lua,重启 Neovim,:Mason 看到自动安装。比自己一个个 google 哪个 LSP 叫什么名字快太多。

进阶:写一个 Lua 函数,让 Claude Code 直接 patch 你的 lspconfig:

function M.suggest_lsp()
  local cwd = vim.fn.getcwd()
  local prompt = string.format(
    "我在 %s 工作。看 README 和文件结构判断技术栈,给出该加进 mason ensure_installed 的 LSP 列表,纯 lua table 格式",
    cwd
  )
  vim.system({ "claude", "-p", prompt }, { text = true, cwd = cwd }, function(obj)
    vim.schedule(function()
      vim.cmd("vnew")
      vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(obj.stdout, "\n"))
      vim.bo.filetype = "lua"
    end)
  end)
end

toggleterm.nvim:浮动终端跑 claude

:terminal 默认开横向 split,挡视野。toggleterm 可以做成浮窗,按一下蹦出来再按一下收起。

-- lua/plugins/toggleterm.lua
require("toggleterm").setup({
  size = function(term)
    if term.direction == "horizontal" then return 15 end
    if term.direction == "vertical" then return vim.o.columns * 0.4 end
  end,
  open_mapping = [[<c-\>]],
  shading_factor = 2,
  direction = "float",
  float_opts = { border = "curved", winblend = 3 },
})

-- 专用的 Claude 终端
local Terminal = require("toggleterm.terminal").Terminal
local claude_term = Terminal:new({
  cmd = "claude",
  direction = "float",
  hidden = true,
  float_opts = { border = "double" },
  on_open = function(term)
    vim.cmd("startinsert!")
    vim.api.nvim_buf_set_keymap(term.bufnr, "t", "<esc>",
      [[<C-\><C-n>]], { noremap = true, silent = true })
  end,
})

function _CLAUDE_TOGGLE()
  claude_term:toggle()
end

vim.keymap.set({ "n", "t" }, "<leader>cc", "<cmd>lua _CLAUDE_TOGGLE()<CR>",
  { desc = "Claude: 浮窗终端" })

<leader>cc 浮窗出来,再按一下隐藏(不退出 Claude 会话)。<esc> 从 terminal mode 回到 normal mode,方便复制内容。


diffview.nvim:可视化看 Claude 的改动

Claude Code 改完一堆文件之后,比起 git diff 在终端里翻,diffview 直接给你三栏:左侧文件列表、中间 before、右侧 after。

require("diffview").setup({
  view = {
    default = { layout = "diff2_horizontal" },
    merge_tool = { layout = "diff3_mixed" },
  },
})

vim.keymap.set("n", "<leader>cd", "<cmd>DiffviewOpen<CR>",
  { desc = "看 Claude 的改动(未提交)" })
vim.keymap.set("n", "<leader>cD", "<cmd>DiffviewOpen HEAD~1<CR>",
  { desc = "看 Claude 上次提交的改动" })
vim.keymap.set("n", "<leader>cq", "<cmd>DiffviewClose<CR>",
  { desc = "关闭 Diffview" })

Claude 跑完之后按 <leader>cd,左侧列出所有改动文件,j/k 切换,主区显示 diff。看到不顺眼的可以直接在 after 那栏改,保存即覆盖。


which-key 注册 Claude Code 快捷键组

快捷键多了记不住,which-key 在你按下 <leader> 后弹出菜单提示。

require("which-key").setup()

require("which-key").register({
  c = {
    name = "+claude",  -- 这会让菜单显示分组名
    c = { "<cmd>lua _CLAUDE_TOGGLE()<CR>", "浮窗终端" },
    e = { "Claude: 解释选中" },         -- visual mode
    r = { "Claude: 重构选中" },
    t = { "Claude: 写测试" },
    f = { "从输出挑文件跳转" },
    d = { "<cmd>DiffviewOpen<CR>", "看改动" },
    D = { "<cmd>DiffviewOpen HEAD~1<CR>", "看上次提交" },
    q = { "<cmd>DiffviewClose<CR>", "关 Diff" },
  },
}, { prefix = "<leader>" })

<leader>c 然后停 0.5 秒,所有 Claude 相关命令全列出来了。


完整 init.lua 片段(可直接抄)

下面是一个把上面所有东西串起来的最小可用配置。把它放到 ~/.config/nvim/init.lua 末尾(或拆成 lua/plugins/claude.lua)。

-- ============ Claude Code 集成 ============
local function get_visual_selection()
  local s_start = vim.fn.getpos("'<")
  local s_end = vim.fn.getpos("'>")
  local lines = vim.api.nvim_buf_get_lines(0, s_start[2] - 1, s_end[2], false)
  if #lines == 0 then return "" end
  lines[1] = string.sub(lines[1], s_start[3])
  lines[#lines] = string.sub(lines[#lines], 1, s_end[3])
  return table.concat(lines, "\n")
end

local function ask_claude(template)
  local code = get_visual_selection()
  if code == "" then return vim.notify("无选区", vim.log.levels.WARN) end
  vim.system(
    { "claude", "-p", template .. "\n\n```\n" .. code .. "\n```" },
    { text = true },
    function(obj)
      vim.schedule(function()
        vim.cmd("vnew")
        vim.bo.buftype = "nofile"
        vim.bo.filetype = "markdown"
        vim.api.nvim_buf_set_lines(0, 0, -1, false,
          vim.split(obj.stdout or "(空)", "\n"))
      end)
    end)
end

vim.keymap.set("v", "<leader>ce", function()
  ask_claude("用中文解释这段代码")
end)
vim.keymap.set("v", "<leader>cr", function()
  ask_claude("重构这段代码并说明改动理由")
end)
vim.keymap.set("v", "<leader>ct", function()
  ask_claude("为这段代码写单测")
end)

-- toggleterm 浮窗
local ok, toggleterm = pcall(require, "toggleterm.terminal")
if ok then
  local ct = toggleterm.Terminal:new({
    cmd = "claude", direction = "float", hidden = true,
    float_opts = { border = "curved" },
  })
  vim.keymap.set({ "n", "t" }, "<leader>cc",
    function() ct:toggle() end, { desc = "Claude 浮窗" })
end

-- diffview
vim.keymap.set("n", "<leader>cd", "<cmd>DiffviewOpen<CR>")
vim.keymap.set("n", "<leader>cq", "<cmd>DiffviewClose<CR>")

只要你的 Neovim 装了 toggleterm.nvim 和 diffview.nvim,这段直接生效。


老 Vim 用户:从 vimscript 到 Lua 的最小迁移

如果你的 .vimrc 还停留在 vimscript,想集成上面这些东西又不想全部重写,最小代价是创建一个 ~/.vim/lua_claude.vim 嵌入 Lua:

" 在传统 .vimrc 里嵌 Lua(Neovim 才有)
if has('nvim')
  lua << EOF
    vim.keymap.set("v", "<leader>ce", function()
      local lines = vim.fn.getline("'<", "'>")
      local code = table.concat(lines, "\n")
      vim.system({"claude", "-p", "解释这段代码:\n" .. code},
        { text = true },
        function(obj)
          vim.schedule(function()
            vim.cmd("vnew")
            vim.api.nvim_buf_set_lines(0, 0, -1, false,
              vim.split(obj.stdout, "\n"))
          end)
        end)
    end)
  EOF
endif

vimscript 部分一行不用动,只是在 if has('nvim') 里夹一段 Lua。先尝鲜,觉得好用再考虑全面迁移。


一个真实工作流:调试 Bug 全流程

1. :cd ~/projects/myapp
2. <leader>cc          # 浮窗开 claude
3. 在 claude 里:
     "RecoveryQueue 在并发情况下偶尔死锁,扫一下相关文件"
4. claude 回复:
     "可疑文件:src/queue.rs:120, src/worker.rs:45, tests/concurrent.rs"
5. <C-\>              # 关浮窗
6. :%y+               # 把 claude 输出复制到剪贴板(其实直接选区拷也行)
7. <leader>cf         # Telescope 弹窗,列出三个文件
8. 选 src/queue.rs:120 回车跳进去
9. 框出嫌疑函数,<leader>ce 让 claude 解释
10. 在右侧 markdown buffer 看分析
11. <leader>cc 回 claude 终端:
      "在 queue.rs 的 lock_two_mutexes 里加超时和重试"
12. claude 改完,<leader>cd 打开 diffview 审查
13. 满意就 :G commit -m "...",不满意就在 diffview 里手改

整个流程鼠标几乎用不上,全键盘。这就是 Neovim 加 Claude Code 真正的价值——不是把 AI 嵌进编辑器,而是让 AI 成为你键盘流的一部分。


相关阅读

其他编辑器工作流:

相关编辑器安装教程: