目录

从零开始配置 Neovim (Neovim v0.12)

Info

更新

  • 2026-04-06,适配 Neovim v0.12,替换 lazy.nvim 为原生插件管理器 vim.pack.add,迁移的 PR 可以在这里找到
  • 2025-09-06,去掉 nvim-lspconfig 插件,因为我更倾向于直接用自带的 LSP 配置
  • 2025-05-14,适配 Neovim v0.11 了,简化 LSP 配置
  • 2025-03-22,将 nvim-cmp 替换为 blink.cmp,配置更简单了
  • 2024-04-04,重写部分章节,Package Manager 从 packer.nvim 换成了 lazy.nvim 🤗。迁移的 commit 可以在这里找到,修改完之后记得用 :checkhealth lazy 检查下,因为还需要删除 packer.nvim 的一些旧文件

进一步阅读:

  • 如何为一门新的编程语言配置 Neovim ,以及支持第三方代码格式化工具,见下篇文章

我使用的是 MacBook Air M3,系统版本为 macOS 26.4。我的 nvim 版本信息如下

NVIM v0.12.0
Build type: Release
LuaJIT 2.1.1741730670
Vim versions: 8.1, 8.2, 9.0, 9.1, 9.2

   system vimrc file: "$VIM/sysinit.vim"
  fall-back for $VIM: "
/nix/store/qzlmii7hrm51ic46rdm1imvbaqa0m9my-neovim-unwrapped-0.12.0/share/nvim
"

Run :checkhealth for more info

在使用 Vim 一年多之后,我越发觉得 Vim 的配置麻烦,启动加载速度也不尽人意。Vimscript 的写法我也不喜欢,所以我决定开始使用 Neovim(nvim)。相比于迁移 Vim 的配置,我决定重新配置 nvim。为什么会想要重新配置而不是迁移配置呢?因为我想顺便趁着这个机会,重新审视我本来 Vim 的配置,以及将本来用到的的插件替换为现在的 Best Practice。我自从看完 MIT 的 Missing semester 的课配置了 ~/.vimrc 之后就很长时间都没有再编辑 ~/.vimrc 文件了

我认为在配置 nvim 的时候弄清楚每一个选项的意思是很有必要的,因此我在这篇博客中会尽量解释清楚每个选项的含义,同时将解释放在注释里面,即我尽量让我自己的配置文件是 self-contained 而且可读性强的

Tip

当然,这难免有疏漏。别忘了我们永远可以在 nvim 里面输入 :h <name> 来看到更为详细的解释

Note

💡 该篇博客假定你对 Vim 有基本了解

在配置 nvim 的时候,我会尽可能用 Lua 语言写配置,因此你有必要了解一下 Lua 的基本语法和语义。可以快速浏览一下 Learn Lua in Y minutes 了解大概

在 Linux/Mac 系统上,nvim 的配置目录在 ~/.config/nvim 下,这个路径也叫做 runtimepathnvim 会默认读取 ~/.config/nvim/init.lua 文件,理论上来说可以将所有配置的东西都放在这个文件里面,但这样不是一个好的做法,因此我划分不同的文件和目录来分管不同的配置

首先看下按照本篇教程配置 nvim 之后,目录结构看起来会是怎么样⬇️

.
├── init.lua
├── lsp
│   └── ty.lua
├── lua
│   ├── keymaps.lua
│   ├── lsp.lua
│   └── options.lua
├── nvim-pack-lock.json
└── plugin
    ├── blink.cmp.lua
    ├── mason-nvim.lua
    └── monokai.nvim.lua

解释如下

  • init.luanvim 配置的 Entry point,我们主要用来导入其他 *.lua 文件
    • keymaps.lua 配置按键映射
    • lsp.lua 配置 LSP
    • options.lua 配置 Neovim 的选项
  • nvim-pack-lock.json:使用原生插件管理器 vim.pack.add 会自动生成一个 Lock File,用于锁定插件版本,方便进行版本控制,你不应该直接修改这个文件
  • lsp 目录,该目录的每一个文件都对应一个 LSP 的配置,后面会具体介绍。在这个例子里,我只设置了一个 Python 的 LSP,叫作 ty
  • lua 目录,当我们在 Lua 里面调用 require 加载模块(文件)的时候,它会自动在 lua 文件夹里面进行搜索。入参的写法:将文件的路径分隔符从 / 替换为 .,然后去掉 .lua 后缀就得到了 require 的入参格式
  • plugin 目录,该目录的每一个文件都对应一个插件,可以看到这里一共安装了 3 个插件,后面我们会看到每一个的功能是啥

主要用到的就是 vim.gvim.optvim.cmd 等,我制造了一个快速参照对比的表格

In Vim In nvim Note
let g:foo = bar vim.g.foo = bar
set foo = bar vim.opt.foo = bar set foo = vim.opt.foo = true
some_vimscript vim.cmd(some_vimscript)

nvim 里面进行按键绑定的语法如下,具体的解释可以看 :h vim.keymap.set

vim.keymap.set(<mode>, <key>, <action>, <opts>)
Tip

对于没耐心看完的人,我已经将本文的配置上传到了这里,克隆之后软链接到 ~/.config/nvim 后就可以使用

在阅读了前面一些配置基础之后,现在我们可以从头开始,由简到易一步步配置 nvim

我用的是 Mac,用 Homebrew 安装 nvim 非常容易,只要运行如下命令即可1

$ brew install neovim 

在安装完成之后,如果 ~/.config/nvim 目录不存在,创建目录并新建如下的文件和目录

$ mkdir ~/.config/nvim
$ mkdir ~/.config/nvim/lsp
$ mkdir ~/.config/nvim/lua
$ mkdir ~/.config/nvim/plugin
$ touch ~/.config/nvim/init.lua
Warning

配置文件编辑保存之后,重启 nvim 就能看到效果,后面默认每次小章节配置完成后就重启

选项配置功能一览:

  • 默认采用系统剪贴板,同时支持鼠标操控 nvim
  • Tab 和空格的换算
  • UI 界面
  • “智能”搜索

新建 ~/.config/nvim/lua/options.lua 文件并加入如下内容⬇️

-- Hint: use `:h <option>` to figure out the meaning if needed
vim.opt.clipboard = 'unnamedplus' -- use system clipboard
vim.opt.completeopt = { 'menu', 'menuone', 'noselect' }
vim.opt.mouse = 'a' -- allow the mouse to be used in nvim

-- Tab
vim.opt.tabstop = 4 -- number of visual spaces per TAB
vim.opt.softtabstop = 4 -- number of spaces in tab when editing
vim.opt.shiftwidth = 4 -- insert 4 spaces on a tab
vim.opt.expandtab = true -- tabs are spaces, mainly because of Python

-- UI config
vim.opt.number = true -- show absolute number
vim.opt.relativenumber = true -- add numbers to each line on the left side
vim.opt.cursorline = true -- highlight cursor line underneath the cursor horizontally
vim.opt.splitbelow = true -- open new vertical split bottom
vim.opt.splitright = true -- open new horizontal splits right
-- vim.opt.termguicolors = true        -- enable 24-bit RGB color in the TUI
vim.opt.showmode = false -- we are experienced, wo don't need the "-- INSERT --" mode hint

-- Searching
vim.opt.incsearch = true -- search as characters are entered
vim.opt.hlsearch = false -- do not highlight matches
vim.opt.ignorecase = true -- ignore case in searches by default
vim.opt.smartcase = true -- but make it case sensitive if an uppercase is entered
Warning

... 表示省略了其他部分的代码(为了节省博客的篇幅)

打开 init.lua,用 require 导入刚才写的 options.lua 文件

... -- 省略其他行
require('options')

按键功能一览:

  • <C-h/j/k/l> 快速在多窗口之间移动光标
  • Ctrl + 方向键进行窗口大小的调整
  • 选择模式(Visual Mode)下可以一直用 Tab 或者 Shift-Tab 改变缩进

新建 ~/.config/nvim/lua/keymaps.lua 文件并放入如下内容⬇️

-- define common options
local opts = {
    noremap = true,      -- non-recursive
    silent = true,       -- do not show message
}

-----------------
-- Normal mode --
-----------------

-- Hint: see `:h vim.map.set()`
-- Better window navigation
vim.keymap.set('n', '<C-h>', '<C-w>h', opts)
vim.keymap.set('n', '<C-j>', '<C-w>j', opts)
vim.keymap.set('n', '<C-k>', '<C-w>k', opts)
vim.keymap.set('n', '<C-l>', '<C-w>l', opts)

-- Resize with arrows
-- delta: 2 lines
vim.keymap.set('n', '<C-Up>', ':resize -2<CR>', opts)
vim.keymap.set('n', '<C-Down>', ':resize +2<CR>', opts)
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', opts)
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', opts)

-----------------
-- Visual mode --
-----------------

-- Hint: start visual mode with the same area as the previous area and the same mode
vim.keymap.set('v', '<', '<gv', opts)
vim.keymap.set('v', '>', '>gv', opts)

然后在 init.lua 文件里面再次加上一行导入这个文件

... -- 省略其他行
require('keymaps')

从 Neovim v0.12 开始,Neovim 有了自己的插件管理器,只需要使用 vim.pack.add 就可以安装插件,这是帮助文档里面的用法示意

vim.pack.add({
  -- Install "plugin1" and use default branch (usually `main` or `master`)
  'https://github.com/user/plugin1',

  -- Same as above, but using a table (allows setting other options)
  { src = 'https://github.com/user/plugin1' },

  -- Specify plugin's name (here the plugin will be called "plugin2"
  -- instead of "generic-name")
  { src = 'https://github.com/user/generic-name', name = 'plugin2' },

  -- Specify version to follow during install and update
  {
    src = 'https://github.com/user/plugin3',
    -- Version constraint, see |vim.version.range()|
    version = vim.version.range('1.0'),
  },
  {
    src = 'https://github.com/user/plugin4',
    -- Git branch, tag, or commit hash
    version = 'main',
  },
})
Tip

在 Neovim 启动的时候,它会自动加载 ~/.config/nvim/plugin/ 里的每一个文件。另外,vim.pack.add 的设计使得你可以在多个 .lua 文件里面调用。这意味着我们不必用一个文件声明所有要安装的插件。每个文件单独进行管理更符合模块化设计。

如果将所有的插件全部放在一个 vim.pack.add 下面,不方便后面进行扩展。因此我这里采用了模块化设计:每一个插件对应 ~/.config/nvim/plugin/ 下的一个文件 :)

Warning

macOS 自带的 Terminal.app 只支持 ANSI 256,在安装完 monokai 主题后,你可能会发现显示整个画面变成蓝色。使用颜色支持更丰富的 Terminal 可以解决这个问题(比如 iTerm2Kitty

我喜欢的主题是 monokai.nvim,新增 ~/.config/nvim/plugin/monokai.nvim.lua 文件,填写如下的内容

vim.pack.add({
	{ src = "https://github.com/tanvirtin/monokai.nvim", name = "monokai" },
})
vim.cmd("colorscheme " .. "monokai")

这里我们用 vim.pack.add 新增了 monokai.nvim 插件,并使用 vim.cmd 调用了 colorscheme monokai 命令设置主题。保存更改并重启 Neovim 就可以看到 Neovim 在询问你是否要下载该插件,根据提示确认(按 y)即可。插件安装完成之后就可以看到有 monokai 主题的 Neovim 了

之前本文的自动补全插件采用的是 nvim-cmp,但配置上较为繁琐。现在有了 blink.cmp 插件,配置会比较简单而且自动补全特别快

新增 ~/.config/nvim/plugin/blink.cmp.lua 文件

vim.pack.add({
	{ src = "https://github.com/saghen/blink.cmp", version = "v1", name = "blink.cmp" },
})

require("blink.cmp").setup({
	-- 'default' (recommended) for mappings similar to built-in completions (C-y to accept)
	-- 'super-tab' for mappings similar to vscode (tab to accept)
	-- 'enter' for enter to accept
	-- 'none' for no mappings
	--
	-- All presets have the following mappings:
	-- C-space: Open menu or open docs if already open
	-- C-n/C-p or Up/Down: Select next/previous item
	-- C-e: Hide menu
	-- C-k: Toggle signature help (if signature.enabled = true)
	--
	-- See :h blink-cmp-config-keymap for defining your own keymap
	keymap = {
		-- Each keymap may be a list of commands and/or functions
		preset = "enter",
		-- Select completions
		["<Up>"] = { "select_prev", "fallback" },
		["<Down>"] = { "select_next", "fallback" },
		["<Tab>"] = { "select_next", "fallback" },
		["<S-Tab>"] = { "select_prev", "fallback" },
		-- Scroll documentation
		["<C-b>"] = { "scroll_documentation_up", "fallback" },
		["<C-f>"] = { "scroll_documentation_down", "fallback" },
		-- Show/hide signature
		["<C-k>"] = { "show_signature", "hide_signature", "fallback" },
	},
	appearance = {
		-- 'mono' (default) for 'Nerd Font Mono' or 'normal' for 'Nerd Font'
		-- Adjusts spacing to ensure icons are aligned
		nerd_font_variant = "mono",
	},
	-- Default list of enabled providers defined so that you can extend it
	-- elsewhere in your config, without redefining it, due to `opts_extend`
	sources = {
		default = { "lsp", "path", "snippets", "buffer" },
	},

	-- (Default) Rust fuzzy matcher for typo resistance and significantly better performance
	-- You may use a lua implementation instead by using `implementation = "lua"` or fallback to the lua implementation,
	-- when the Rust fuzzy matcher is not available, by using `implementation = "prefer_rust"`
	--
	-- See the fuzzy documentation for more information
	fuzzy = { implementation = "prefer_rust_with_warning" },
	completion = {
		-- The keyword should only matchh against the text before
		keyword = { range = "prefix" },
		menu = {
			-- Use treesitter to highlight the label text for the given list of sources
			draw = {
				treesitter = { "lsp" },
			},
		},
		-- Show completions after tying a trigger character, defined by the source
		trigger = { show_on_trigger_character = true },
		documentation = {
			-- Show documentation automatically
			auto_show = true,
		},
	},

	-- Signature help when tying
	signature = { enabled = true },
})
Tip

Neovim 插件的一个惯例是:使用 .setup 方法对插件进行配置

这里我们使用 .setup 方法对 blink.cmp 进行配置,具体的配置项是一个 Lua table,这里面的几个关键配置项是

  • keymap - 用于配置按键映射,格式也很好理解
    • preset = "enter" 表示用 回车键 确定当前选中的补全项
    • select_prev, select_next 用于在各个候选项中进行选择,我这里配置了 2 套按键,支持用⬆️/⬇️,或者用 Tab/Shift-Tab 进行补全项的选择
    • scroll_documentation_up, scroll_documentation_down 用于滚动 API 的文档,我配置的是 Ctrl-b, Ctrl-f
  • trigger = { show_on_trigger_character = true } - 输入字符之后就会展示所有可用补全项
  • documentation = { auto_show = true } - 自动显示当前被选中补全项的文档
Note

🎙️ 到这为止,重新启动 nvim 后,等待插件安装完成后应该就能够用初步的自动补全功能了~

配置完 blink.cmp 之后,已经有基本的自动补全功能,但和 IDE 相比,我们还需要定义跳转、代码补全等功能,因此需要配置 LSP2

Tip

事实上不用 mason.nvim 也可以,因为你完全可以使用你操作系统的包管理器下载 LSP,但我发现 mason.nvim 用起来确实方便不少

从 Neovim v0.11 开始引入了 vim.lsp.configvim.lsp.enable 这 2 个 API 对 LSP 进行配置,这下再也不用安装 nvim-lspconfig 了。但我们*可能**还是需要一个 LSP Registry 用来方便地下载 LSP,比如 mason.nvim

新增 ~/.config/nvim/plugin/mason-nvim.lua 文件

vim.pack.add({
	{ src = "https://github.com/mason-org/mason.nvim", name = "mason" },
})
require("mason").setup({
	ui = {
		icons = {
			package_installed = "✓",
			package_pending = "➜",
			package_uninstalled = "✗",
		},
	},
})
Tip

每个 LSP 的配置有 2 种方法

  1. 可以用 vim.lsp.config.<lsp> = { ... } 配置
  2. runtimepath(Linux/macOS 下是 ~/.config/nvim)下创建 lsp 文件夹,在每个 LSP 的配置文件里面直接 return { ... } 后面我会采用第二种写法

接下来我们来看如何配置一个 LSP,其实很简单,只需要 3 个步骤

  1. mason.nvim 安装 LSP,只需要打开 Neovim 输入 :Mason,找到你要的 LSP 并将光标停留在上面,随后按下 i 即可
  2. 使用 vim.lsp.config.<lsp> = { ... } 进行配置,你需要提供如下几个配置项的信息
    • cmd - 如何启动 LSP?
    • filetypes - 这个 LSP 处理哪些类型的文件?
    • root_markers - 被编辑文件的 root 目录(或者说项目根目录)在哪里?同一个项目根目录会链接到同一个 LSP 上
    • settings(可选项)- 每个 LSP 提供了不同的定制项
  3. 使用 vim.lsp.enable({ ... }) 启动 LSP
Tip

不知道 LSP 的配置怎么写?你可以参考 nvim-lspconfig 是怎么写的:https://github.com/neovim/nvim-lspconfig/tree/master/lsp

以 Python 的 LSP 配置为例,它有很多的 LSP 可供选择,我这里选的是 tyuv 背后的团队开发的基于 Rust 的 LSP)。在安装完 ty 之后,在 lsp 目录下新建 ty.lua,内容如下

-- src: https://github.com/neovim/nvim-lspconfig/blob/master/lsp/ty.lua
---@type vim.lsp.Config
return {
  cmd = { 'ty', 'server' },
  filetypes = { 'python' },
  root_markers = { 'ty.toml', 'pyproject.toml', '.git' },
}

让我们新建 ~/.config/nvim/lua/lsp.lua 文件,用于设置 LSP 的按键映射,并启动刚才设置的 LSP

... -- 省略其他行
-- Remove Global Default Key mapping
vim.keymap.del("n", "grn")
vim.keymap.del("n", "gra")
vim.keymap.del("n", "grr")
vim.keymap.del("n", "gri")
vim.keymap.del("n", "gO")

-- Create keymapping
-- LspAttach: After an LSP Client performs "initialize" and attaches to a buffer.
vim.api.nvim_create_autocmd("LspAttach", {
    callback = function (args)
        local keymap = vim.keymap
        local lsp = vim.lsp
	    local bufopts = { noremap = true, silent = true }

        keymap.set("n", "gr", lsp.buf.references, bufopts)
        keymap.set("n", "gd", lsp.buf.definition, bufopts)
        keymap.set("n", "<space>rn", lsp.buf.rename, bufopts)
        keymap.set("n", "K", lsp.buf.hover, bufopts)
        keymap.set("n", "<space>f", function()
            vim.lsp.buf.format({ async = true })
        end, bufopts)
    end
})

vim.lsp.enable({ "ty" })

这里的配置首先用 vim.keymap.del 删除了自带的一些按键映射,然后用 vim.keymap.set 配置具体的按键映射,比如在 Normal Mode 下按 gd 就可以跳转到定义 Symbol 的地方。**如果*你想要增加更多的按键映射,只需要添加新的行即可

最后,记得在 init.lua 文件里面加上如下内容,这样 lsp.lua 的配置才能被加载

... -- 省略其他行
require('lsp')

重启 nvim 之后,你可以打开随意一个 .py 文件并开始编辑,你应该可以看到代码补全等功能都可以正常使用了。如果你不大确定,可以用 :checkhealth vim.lsp 命令进行检查,它应该包含类似的内容

vim.lsp: Active Clients ~
- ty (id: 1)
  - Version: 0.0.26 (940305127 2026-03-26)
  - Root directory: ...
  - Command: { "ty", "server" }
  - Settings: {}
  - Attached buffers: 1

这样配置下来,我们成功把 nvim 变成了一个轻量级的 IDE,它支持代码高亮、代码补全、语法检查等功能,而且是完全开源免费的,虽然还有些简陋,但已经是可以用的了 🤗

我发现自从学了 Vim 之后,我总在其他各种代码编辑器、IDE 看是不是支持 Vim。大多数情况下,它们对 Vim 的支持都不是很让人满意,还容易有快捷键冲突等问题。因此我选择将 nvim 变成 IDE,并将配置文件托管在我的 Martinlwx/dotfiles 上。这样在新的机器上只要安装好 nvim 并克隆配置,静待片刻之后就可以在不同的机器上获得一样的编程体验

打磨定制化工具是需要付出一定精力的。为了理解每个选项都在干啥,我不得不查找各种资料。但我仍相信这是值得的理解你的工具利于你做扩展和定制化。本文已经尽可能采用了比较简单的配置,还有很多美化、私人定制化的内容可以配置,更别提其他很多优秀的第三方插件都还没有提及,这些就留给读者自己去探索发现了