Neovim 配置与使用:LazyVim 精简版 个人备忘
概览
整套配置 = LazyVim + 7 个官方 extras + 少量自定义,只有 5 个文件、约 160 行。搜索、LSP、补全、格式化、文件树、Git 等都用 LazyVim 自带的实现;本地 WSL 和 win-dev 已切换到这套并验证通过。
bash
~/.config/nvim/
├── init.lua # 只有一行 require
└── lua/
├── config/
│ ├── lazy.lua # 引导 lazy.nvim + 选择 LazyVim extras
│ └── options.lua # 全局选项、剪贴板
└── plugins/
├── custom.lua # 只写和 LazyVim 默认不同的地方
└── project.lua # 项目切换(project.nvim)
和 LazyVim 默认不同的地方:
| 文件 | 改动 | 作用 |
|---|---|---|
custom.lua |
Go 用 goimports,gopls 关掉 gofumpt | 团队用标准 gofmt,避免无关 diff |
custom.lua |
Python 保存时 ruff 整理 import + 格式化 | import 自动排序 |
custom.lua |
golangci-lint 在文件所属 go.mod 目录运行,检查整个包 | 从仓库根目录打开(go.mod 在子目录)时不再误报 could not import / undefined |
custom.lua |
fzf-lua rg_glob = true |
全文搜索支持 关键词 -- *.go |
custom.lua |
mini.map 右侧代码缩略图,启动时自动打开 | 显示搜索匹配、git 改动、诊断位置 |
options.lua |
相对行号、GBK 编码识别、忽略大小写 + smartcase | 中文旧文件不乱码 |
options.lua |
剪贴板走 OSC 52 | 复制经终端(含 tmux、ssh)写到本机剪贴板 |
project.lua |
project.nvim 自动切换到项目根目录 + fzf 项目列表 | 打开文件即进入所在项目,<leader>fp 在最近项目间切换 |
旧的手写配置备份在 ~/.config/nvim.bak-lazyvim。
Mac 快速搭建
大约 10 分钟,不需要 git 仓库。
-
装外部工具
bashxcode-select --install brew install neovim git ripgrep fd fzf lazygit node go python brew install --cask font-jetbrains-mono-nerd-font -
复制下文「配置脚本」代码块,在终端执行(已有的
~/.config/nvim会先改名备份):bashpbpaste > ~/setup-nvim.sh && bash ~/setup-nvim.sh -
装插件,然后正常打开一次 nvim,等 Mason 自动装完 gopls、pyright、ruff、vtsls、prettier、golangci-lint、tree-sitter 等工具(
:Mason看进度,约 2--3 分钟):bashnvim --headless "+Lazy! sync" +qa nvim -
验证:打开一个 Go 项目,
gd能跳定义;<leader>/输入func -- *.go只出 .go 结果;改乱 Python 的 import 顺序后保存会自动排好;:checkhealth没有红色错误。
终端设置:iTerm2 / Ghostty 里把字体设为 JetBrainsMono Nerd Font,并把 Option 键设为 Meta(iTerm2:Profiles → Keys → Left Option = Esc+;Ghostty:macos-option-as-alt = true),否则 Alt-j/k 移动行不生效。
配置脚本
下面就是完整配置,每个 cat > ... <<'EOF' 块就是一个文件的全部内容,也可以手动建这 5 个文件。按「Mac 快速搭建」第 2 步执行。
bash
#!/usr/bin/env bash
# 写出 LazyVim 精简版 Neovim 配置(5 个文件);已有 ~/.config/nvim 会先备份
set -e
C="$HOME/.config/nvim"
[ -e "$C" ] && mv "$C" "$C.bak-$(date +%Y%m%d%H%M%S)"
mkdir -p "$C/lua/config" "$C/lua/plugins"
cat > "$C/init.lua" <<'EOF'
require("config.lazy")
EOF
cat > "$C/lua/config/lazy.lua" <<'EOF'
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", "https://github.com/folke/lazy.nvim.git", lazypath })
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
spec = {
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
-- 语言与工具扩展(等价于原来手写的 LSP / 格式化 / 搜索配置)
{ import = "lazyvim.plugins.extras.editor.fzf" },
{ import = "lazyvim.plugins.extras.lang.python" },
{ import = "lazyvim.plugins.extras.lang.typescript" },
{ import = "lazyvim.plugins.extras.lang.go" },
{ import = "lazyvim.plugins.extras.lang.json" },
{ import = "lazyvim.plugins.extras.formatting.prettier" },
{ import = "lazyvim.plugins.extras.linting.eslint" },
{ import = "plugins" },
},
checker = { enabled = false },
})
EOF
cat > "$C/lua/config/options.lua" <<'EOF'
vim.g.lazyvim_picker = "fzf" -- 搜索用 fzf-lua
vim.g.lazyvim_prettier_needs_config = true -- 项目里有 .prettierrc 才用 prettier 格式化
vim.g.lazyvim_python_lsp = "pyright"
vim.g.lazyvim_python_ruff = "ruff"
vim.opt.relativenumber = true
vim.opt.fileencodings = { "ucs-bom", "utf-8", "gbk", "gb18030", "latin1" }
-- 搜索行为
vim.o.incsearch = true -- 输入时实时预览
vim.o.hlsearch = true -- 搜索后高亮所有匹配
vim.o.ignorecase = true -- 忽略大小写
vim.o.smartcase = true -- 含大写字母时区分大小写
-- 剪贴板:复制通过 OSC 52 写到终端(经 tmux 转发到 Windows 剪贴板)
local osc52 = require("vim.ui.clipboard.osc52")
local function paste_unnamed()
-- 终端不支持读取 OSC 52 时,直接用 nvim 内部的无名寄存器,避免 p 卡住等待
return { vim.fn.split(vim.fn.getreg('"'), "\n"), vim.fn.getregtype('"') }
end
vim.g.clipboard = {
name = "OSC 52",
copy = { ["+"] = osc52.copy("+"), ["*"] = osc52.copy("*") },
-- tmux 会用自己的缓冲区回应 OSC 52 读取请求,所以在 tmux 里可以真正共享
paste = vim.env.TMUX
and { ["+"] = osc52.paste("+"), ["*"] = osc52.paste("*") }
or { ["+"] = paste_unnamed, ["*"] = paste_unnamed },
}
vim.opt.clipboard = "unnamedplus"
EOF
cat > "$C/lua/plugins/custom.lua" <<'EOF'
return {
-- 格式化:Go 用 goimports(标准 gofmt 风格);Python 保存时整理 import + 格式化
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
go = { "goimports" },
python = { "ruff_organize_imports", "ruff_format" },
},
},
},
-- gopls:团队用标准 gofmt,不用 gofumpt
{
"neovim/nvim-lspconfig",
opts = { servers = { gopls = { settings = { gopls = { gofumpt = false } } } } },
},
-- golangci-lint 在当前文件所属 go.mod 目录下运行:默认用 nvim 的 cwd,
-- 从仓库根目录打开时(go.mod 在 backend/src)会误报 could not import / undefined
{
"mfussenegger/nvim-lint",
opts = {
linters = {
golangcilint = function()
local linter = vim.deepcopy(require("lint.linters.golangcilint"))
local root = vim.fs.root(0, "go.mod")
if root then
linter.cwd = root
-- nvim-lint 按启动 cwd 判断有无 go.mod,仓库根目录下会退化为只检查单个文件,
-- 同包其他文件里的定义随之报 undefined;这里改回检查整个包目录
linter.args[#linter.args] = function()
return vim.fn.expand("%:p:h")
end
end
return linter
end,
},
},
},
-- live_grep 支持 `关键词 -- *.go` 过滤
{
"ibhagwan/fzf-lua",
opts = { grep = { rg_glob = true } },
},
-- 右侧代码缩略图:显示搜索匹配、git 改动和诊断标记
{
"nvim-mini/mini.map",
version = false, -- 使用最新开发版
event = "VeryLazy",
config = function()
local map = require("mini.map")
map.setup({
integrations = {
map.gen_integration.builtin_search(),
-- LazyVim 默认用 gitsigns 显示 git 改动,没有 mini.diff,因此用 gitsigns 集成
map.gen_integration.gitsigns(),
map.gen_integration.diagnostic(),
},
-- 默认的 3x2 块字符很多终端字体缺字形,会显示成大方块;改用盲文点阵,兼容性好
symbols = { encode = map.gen_encode_symbols.dot("4x2") },
})
vim.keymap.set("n", "<Leader>mm", MiniMap.toggle, { desc = "Toggle minimap" })
-- 默认不会自动显示,启动后主动打开
MiniMap.open()
end,
},
}
EOF
cat > "$C/lua/plugins/project.lua" <<'EOF'
-- 项目管理(ahmedkhalf/project.nvim):打开文件时自动 cd 到所在项目根目录,<leader>fp 列出最近的项目
return {
"ahmedkhalf/project.nvim",
-- 插件的 Lua 模块名是 project_nvim,和仓库名对不上,不写 main 的话 lazy.nvim 不会调用 setup,插件等于没启用
main = "project_nvim",
-- 启动时就加载:要靠它的 BufEnter 自动命令切换目录;配了 keys 后 lazy.nvim 默认会改成按键时才加载
lazy = false,
opts = {
manual_mode = false,
-- 只按 .git 等文件识别项目:LSP 识别在 monorepo 里会把 cwd 切到 backend/src,且插件的 LSP 代码用的是已废弃 API
detection_methods = { "pattern" },
},
keys = {
{
"<leader>fp",
function()
local fzf = require("fzf-lua")
local projects = vim.fn.reverse(require("project_nvim").get_recent_projects()) -- 最近的在前
fzf.fzf_exec(projects, {
prompt = "Projects> ",
actions = {
-- 回车:切到该项目目录,并打开文件树(已开着的旧文件树先关掉,避免还停在上个项目)
["default"] = function(sel)
vim.api.nvim_set_current_dir(sel[1])
for _, picker in ipairs(Snacks.picker.get({ source = "explorer" })) do
picker:close()
end
Snacks.explorer({ cwd = sel[1] })
end,
-- ctrl-f:切到该项目并在里面找文件
["ctrl-f"] = function(sel)
vim.api.nvim_set_current_dir(sel[1])
fzf.files({ cwd = sel[1] })
end,
-- ctrl-s:在该项目里全文搜索
["ctrl-s"] = function(sel)
vim.api.nvim_set_current_dir(sel[1])
fzf.live_grep({ cwd = sel[1] })
end,
},
})
end,
desc = "Projects",
},
},
}
EOF
echo "已写入 $C,下一步:nvim --headless \"+Lazy! sync\" +qa"
LSP、格式化与搜索
保存时自动格式化;<leader>cf 手动格式化,<leader>uf 开关保存时格式化。工具全由 Mason 自动安装。
| 语言 | LSP | 保存时格式化 | Lint |
|---|---|---|---|
| Go | gopls(gofumpt 关闭) | goimports | golangci-lint |
| Python | pyright + ruff | ruff_organize_imports → ruff_format | ruff |
| JS / TS | vtsls | prettier(仅项目有 .prettierrc 时) | eslint |
| JSON | jsonls | prettier(同上) | --- |
全文搜索 <leader>/ 支持 glob 过滤,-- 前后都要留空格:
go
create -- *.go # 只搜 .go
create -- *.go !*_test.go # 排除测试文件
TODO -- src/**/*.ts # 限定目录
fzf 窗口内:Ctrl-j/k 上下移动,Ctrl-q 把所有结果发到 quickfix,Alt-i / Alt-h 切换是否包含被忽略 / 隐藏文件,Ctrl-r 在项目根目录和当前目录间切换,Esc 关闭。
搜索指定目录
最常用的是在 <leader>/ 的 -- 后写路径 glob,路径相对项目根目录:
lua
create -- backend/** # 只搜 backend 目录
create -- backend/src/**/*.go # backend/src 下的 .go
create -- backend/** !**/*_test.go # backend 下,排除测试文件
create -- !web/** # 排除 web 目录
| 其他做法 | 写法 |
|---|---|
| 搜索根目录直接设为某目录 | :FzfLua live_grep cwd=backend/src、:FzfLua files cwd=backend/src |
| 在项目根目录 / nvim 当前目录间切换 | fzf 窗口里按 Ctrl-r |
| 按 nvim 当前目录搜索 | 先 :cd backend/src,再 <leader>sG(全文)或 <leader>fF(文件) |
搜索替换(grug-far)
<leader>sr 打开替换面板;visual 模式下选中文字再按,会自动填进 Search。各栏输入后下方实时显示匹配和替换预览。
| 栏 | 填什么 |
|---|---|
| Search | 要搜的内容,默认是 rg 正则 |
| Replace | 替换成什么,正则分组用 $1 引用 |
| Files Filter | 文件过滤,如 *.go;默认自动填当前文件的扩展名 |
| Flags | rg 参数:-i 忽略大小写、-F 按普通文本匹配、-w 整词匹配 |
| Paths | 限定目录,如 backend/src |
确认预览后回到 normal 模式操作(<localleader> 是 \):
| 按键 | 作用 |
|---|---|
\r |
执行全部替换 |
Enter / \i |
跳到 / 预览光标所在结果的位置 |
\j |
只替换下一处 |
dd,然后 \s |
在结果里删掉不想改的行,只把剩下的应用到文件 |
\q / \t / \c |
发到 quickfix / 历史记录 / 关闭 |
g? |
查看全部按键 |
例:把 backend 下所有 Go 文件里的 oldName 换成 newName。
<leader>sr- Search 填
oldName,Replace 填newName - Files Filter 填
*.go,Paths 填backend - 看一眼预览,
Esc回 normal 模式,按\r
只改当前文件用 Vim 自带的 :%s/old/new/gc 更快,c 表示每处都确认。
Git:lazygit 与内置 Git 命令
LazyVim 自带 lazygit 集成,只要装了 lazygit 命令就能用(Mac:brew install lazygit;Ubuntu:sudo apt install lazygit)。<leader>gg 在浮窗里打开 lazygit,q 退出回到 nvim。
| 快捷键 | 作用 |
|---|---|
<leader>gg / <leader>gG |
lazygit(项目根目录 / nvim 当前目录) |
<leader>gf |
当前文件的提交历史 |
<leader>gl / <leader>gL |
提交日志(项目根目录 / 当前目录) |
<leader>gb |
当前行是谁、哪次提交改的(blame) |
<leader>gs / <leader>gd |
Git 状态 / 改动的文件(fzf 列表) |
<leader>gS |
stash 列表 |
<leader>gB |
在浏览器里打开当前文件的远程页面 |
lazygit 里的常用键:空格 暂存 / 取消暂存文件,a 全部暂存,c 提交,P push,p pull,e 在 nvim 里打开文件,1--5 切换左侧面板,? 查看全部按键。
项目切换(project.nvim)
打开任意文件时,nvim 的当前目录会自动切到该文件所在的项目根目录(按 .git 等文件识别),并把项目记入历史。<leader>fp 列出最近的项目,最近的在前:
| 按键 | 作用 |
|---|---|
Enter |
进入项目:切换目录并打开文件树 |
Ctrl-f |
进入项目并在里面找文件 |
Ctrl-s |
进入项目并在里面全文搜索 |
新项目要先打开过其中的文件才会出现在列表里;历史存在 ~/.local/share/nvim/project_nvim/project_history,每行一个路径,可以直接编辑删掉不要的。
自动切换目录后,<leader>fF、<leader>sG 这类「按当前目录」的命令,范围就是当前文件所在的项目。
配置要点:插件的 Lua 模块名是 project_nvim,必须写 main = "project_nvim",否则 lazy.nvim 不会调用 setup、插件不生效;还要 lazy = false,否则配了快捷键后它只在按 <leader>fp 时才加载,打开文件不会切目录。维护分支 DrKJeff16/project.nvim 目前有 bug(打开文件不自动切目录、项目菜单报错),所以用的是原版 ahmedkhalf/project.nvim。
剪贴板、缩略图与 Go lint
剪贴板(OSC 52) :y 复制的内容经终端写到本机剪贴板,在 tmux 里、ssh 到 win-dev 上都有效,不依赖 win32yank / pbcopy。粘贴分两种情况:
| 场景 | p 粘贴的内容 |
粘贴外部(浏览器等)复制的内容 |
|---|---|---|
| 在 tmux 里 | tmux 缓冲区(nvim / tmux 里复制过的) | 插入模式下用终端粘贴:Windows Terminal Ctrl-Shift-V,Mac Cmd-V |
| 不在 tmux 里 | nvim 内部最近复制的内容(避免 p 卡住等待) |
同上 |
tmux 需要 set -s set-clipboard on(本机的 tmux 已是 on)。Mac 上 iTerm2 要在 Settings → General → Selection 勾上「Applications in terminal may access clipboard」;Ghostty 默认允许写入。
代码缩略图(mini.map) :启动后右侧自动显示,标出搜索匹配、git 改动和诊断的位置;<leader>mm 开关。显示用盲文点阵字符,如果是大方块,说明终端字体不是 Nerd Font。
Go lint(golangci-lint) :保存时在当前文件所属的 go.mod 目录下检查整个包。所以直接在仓库根目录 nvim . 就行(如 go.mod 在 backend/src),不用先 cd 到 go.mod 所在目录。
快捷键速查
<leader> = 空格。按下空格稍等会弹出 which-key 提示,忘了就用 <leader>sk 搜索。以下都是 LazyVim 默认键位。
| 分类 | 快捷键 | 作用 |
|---|---|---|
| 文件 | <leader><space> / <leader>ff |
查找文件(项目根目录) |
| 文件 | <leader>fr |
最近文件 |
| 文件 | <leader>, / <leader>fb |
切换 buffer |
| 文件 | <leader>e |
文件树 |
| 搜索 | <leader>/ / <leader>sg |
全文搜索(支持 glob) |
| 搜索 | <leader>sw |
搜索光标词 / 选中内容 |
| 搜索 | <leader>sR |
恢复上次搜索 |
| 搜索 | <leader>sr |
全局搜索替换 |
| 搜索 | <leader>ss / <leader>sS |
文件符号 / 工作区符号 |
| 搜索 | <leader>sd / <leader>sD |
诊断(项目 / 当前文件) |
| 搜索 | <leader>st |
TODO 列表 |
| LSP | gd / gr / gI / gy |
定义 / 引用 / 实现 / 类型定义 |
| LSP | Ctrl-o / Ctrl-i |
跳转后后退 / 前进 |
| LSP | K |
悬浮文档 |
| LSP | <leader>ca / <leader>cr |
代码操作 / 重命名 |
| LSP | ]d / [d / <leader>cd |
下一个 / 上一个 / 当前行诊断 |
| LSP | <leader>xx |
诊断列表(Trouble) |
| 格式化 | <leader>cf / <leader>uf |
手动格式化 / 开关保存时格式化 |
| Git | <leader>gg |
lazygit |
| Git | <leader>gs |
Git 状态 |
| 编辑 | s |
Flash 跳转 |
| 编辑 | Alt-j / Alt-k |
下移 / 上移行 |
| 窗口 | Shift-h / Shift-l |
上一个 / 下一个 buffer |
| 窗口 | <leader>bd |
关闭 buffer |
| 窗口 | <leader>- / ` |
` |
| 其他 | Ctrl-s / <leader>qq |
保存 / 退出全部 |
| 其他 | <leader>l / <leader>cm |
插件管理 Lazy / Mason |
| 其他 | <leader>mm |
开关代码缩略图 |
| 文件 | <leader>fp |
最近项目列表,回车进入并打开文件树 |
常见问题
| 现象 | 原因 / 处理 |
|---|---|
gd 没反应、没有补全 |
LSP 还没装好或没启动::Mason 看安装状态,:LspInfo 看是否已挂上 |
golangci-lint ENOENT |
Go 的 linter 还在安装,等 Mason 装完即可 |
| treesitter 报 tree-sitter CLI 缺失 | 同上,Mason 会自动装 tree-sitter-cli;失败就在 :Mason 里重装 |
| JS/TS 保存不格式化 | 项目没有 .prettierrc;想总是格式化就删掉 options.lua 里的 lazyvim_prettier_needs_config |
-- *.go 搜不到 |
-- 前后要有空格;修改配置后要重启 nvim |
| Alt 快捷键不生效(Mac) | 终端里把 Option 键设为 Meta,见「Mac 快速搭建」 |
| 想回到旧的手写配置 | mv ~/.config/nvim ~/.config/nvim.lazyvim && mv ~/.config/nvim.bak-lazyvim ~/.config/nvim,再执行 :Lazy restore |
| Go 文件报 could not import / undefined | golangci-lint 没在 go.mod 目录运行;本配置已处理,仍出现就确认 custom.lua 里的 nvim-lint 段落还在,并重启 nvim |
| y 复制后系统剪贴板里没有 | 终端没开 OSC 52:tmux 加 set -s set-clipboard on,iTerm2 勾上允许访问剪贴板 |
| 缩略图显示成大方块 | 终端字体换成 Nerd Font;不想要就 <leader>mm 关掉 |
要加功能先看 LazyVim 有没有对应 extra::LazyExtras 里按 x 启用,不用写代码。