目录

从零开始配置 Neovim(Nvim)

Info

更新

  • 2025-05-14,Neovim 升级到 v0.11 了,对 LSP 的配置进行了简化
  • 2025-03-22,将 nvim-cmp 替换为 blink.cmp,配置更简单了
  • 2024-04-04,重写部分章节,Package Manager 从 packer.nvim 换成了 lazy.nvim 🤗。
    • 如果之前跟着本篇教程采用 packer.nvim 进行安装的话,可以参考我迁移的时候的 commit,修改完之后记得用 :checkhealth lazy 检查下,因为还需要删除 packer.nvim 的一些旧文件

进一步阅读:

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

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

NVIM v0.11.1
Build type: Release
LuaJIT 2.1.1744318430

   system vimrc file: "$VIM/sysinit.vim"
  fall-back for $VIM: "/opt/homebrew/Cellar/neovim/0.11.1/share/nvim"

Run :checkhealth for more info

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

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

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

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

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

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

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

.
├── init.lua
└── lua
    ├── colorscheme.lua
    ├── keymaps.lua
    ├── lsp.lua
    ├── options.lua
    └── plugins.lua

解释如下

  • init.luaNvim 配置的 Entry point,我们主要用来导入其他 *.lua 文件
    • colorscheme.lua 配置主题
    • keymaps.lua 配置按键映射
    • lsp.lua 配置 LSP
    • options.lua 配置选项
    • plugins.lua 配置插件
  • lua 目录。当我们在 Lua 里面调用 require 加载模块(文件)的时候,它会自动在 lua 文件夹里面进行搜索
    • 将路径分隔符从 / 替换为 .,然后去掉 .lua 后缀就得到了 require 的参数格式

主要用到的就是 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>)

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

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

$ brew install neovim 

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

$ mkdir ~/.config/nvim
$ mkdir ~/.config/nvim/lua
$ touch ~/.config/nvim/init.lua

💡 配置文件编辑保存之后,重启 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 spacesin 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        -- enabl 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

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

require('options')

按键功能一览:

  • <C-h/j/k/l> 快速在多窗口之间移动光标
  • Ctrl + 方向键进行窗口大小的调整
  • 选择模式下可以一直用 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')
警告
... 表示省略了其他部分的代码(为了节省博客的篇幅)

一个强大的 Nvim 离不开插件的支持。我选用的是当下最为流行 lazy.nvim。它支持如下许多特性:

  • 正确处理不同插件之间的依赖
  • 支持定制 Lazy loading,比如基于 Event、Filetype 等

新建 ~/.config/nvim/lua/plugins.lua 文件并放入如下内容。下面的模板只完成了 lazy.nvim 自身的安装,还没有指定其他第三方插件

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", -- latest stable release
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({})

💡 在 lazy.nvim 指定第三方插件很简单,只需要在 require("lazy").setup({ ... })... 里面声明插件

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

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

此时你重启 Nvim 会发现黑屏没显示,这是因为 lazy.nvim 在安装自己,静待片刻即可☕️。等待 Dashboard 出现之后,可以输入 :Lazy 试试,如果看到了弹出了 lazy.nvim 的窗口,那就安装成功了🎉

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

我喜欢的主题是 monokai.nvim,在 plugins.lua 进行修改

... -- 省略其他行
require("lazy").setup({
    "tanvirtin/monokai.nvim",
})

保存更改并重启就可以看到 lazy.nvim 在帮我们安装插件了,新建并编辑 ~/.config/nvim/lua/colorscheme.lua 文件

-- define your colorscheme here
local colorscheme = 'monokai_pro'

local is_ok, _ = pcall(vim.cmd, "colorscheme " .. colorscheme)
if not is_ok then
    vim.notify('colorscheme ' .. colorscheme .. ' not found!')
    return
end

这里用到的 pcall 是 Lua 里面的 protected call,它会返回一个 bool 变量表示是否执行成功(跟 Go 语言的 err 功能类似)。这里采用 pcall 而不是直接在 init.lua 文件里面加上 vim.cmd('colorscheme monokai_pro') 是为了避免主题没有安装的情况下打开 Nvim 看到一大堆报错信息2

最后在 init.lua 文件里面导入就行

... -- 省略其他行
require('colorscheme')
Warning

blink.cmp 还在 beta 版本,这意味着变动会比较大,而且可能会遇到不少 Bug。但我目前日常使用下来没有问题 :)

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

plugins.lua里新增这个插件并做好配置

... -- 省略其他行
require("lazy").setup({
    ... -- 省略其他行
    {
        "saghen/blink.cmp",
        -- optional: provides snippets for the snippet source
        dependencies = { "rafamadriz/friendly-snippets" },

        -- use a release tag to download pre-built binaries
        version = "*",
        -- AND/OR build from source, requires nightly: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust
        -- build = 'cargo build --release',
        -- If you use nix, you can build from source using the latest nightly rust with:
        -- build = 'nix run .#build-plugin',

        opts = {
            -- '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",
            },

            sources = {
                -- `lsp`, `buffer`, `snippets`, `path`, and `omni` are built-in
                -- so you don't need to define them in `sources.providers`
                default = { "lsp", "path", "snippets", "buffer" },

                -- Sources are configured via the sources.providers table
            },

            -- (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 match 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 typing 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 },
        },
        opts_extend = { "sources.default" },
    }
})

opts = { ... } 用于对插件进行具体的配置,{ ... } 里的配置内容会被自动发送给对应的插件。关键的几个解释如下

  • 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 } - 自动显示当前被选中补全项的文档

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

配置完 blink.cmp 之后,已经有基本的自动补全功能,但和 IDE 相比,我们还需要定义跳转、代码补全等功能,因此需要配置 LSP3。我选择搭配使用的工具是 mason.nvimmason-lspconfig.nvim,他们的功能分别是

首先修改 plugins.lua 文件,增加这两个插件

... -- 省略其他行
require("lazy").setup({
	-- LSP manager
	{ "mason-org/mason.nvim", opts = {} },
    {
        "mason-org/mason-lspconfig.nvim",
        dependencies = {
            "mason-org/mason.nvim",
            "neovim/nvim-lspconfig",
        },
        opts = {
            ensure_installed = { "pylsp" },
        },
    },
    ... -- 省略其他行
})

mason.nvim 采用默认配置即可,所以用的是 opts = {}mason-lspconfig.nvim 的配置项我使用 ensure_installed 确保 pylsp 会被自动安装,pylsp 是 Python 语言的一个 LSP。接下来,需要通过 nvim-lspconfigpylsp 进行配置,继续修改 plugins.lua 文件,增加 nvim-lspconfig 插件并配置 pylsp

... -- 省略其他行
require("lazy").setup({
	{
		"neovim/nvim-lspconfig",
		config = function()
			local lspconfig = require("lspconfig")

			lspconfig.pylsp.setup({})
		end,
	},
    ... -- 省略其他行
})

这里我们没有采用 opts = { ... } 进行插件配置,而是使用 config = function() ... end 自定义一个配置函数,该函数会被自动执行。这里主要的功能是调用 lspconfig.pylsp.setup({}),这里的 {} 表示我们采用该 LSP 的默认行为

Tip

如果你想要修改某个 LSP 的默认行为,可以查看 nvim-lspconfig官方指导,找到对应的 LSP 查看具体支持的配置项

完成上面的配置之后,LSP 其实已经可用了。但如果你也跟我一样,想要对按键映射做一些定制化的化,可以新建一个 ~/.config/nvim/lua/lsp.lua 文件并编辑,增加如下的内容

... -- 省略其他行
-- 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()
            require("conform").format({ async = true, lsp_fallback = true })
        end, bufopts)
    end
})

这里的配置首先用 vim.keymap.del 删除了自带的一些按键映射,然后用 vim.keymap.set 配置具体的按键映射,比如在 Normal Mode 下按 gd 就可以跳转到定义 Symbo 的地方。如果你想要增加更多的按键映射,只需要添加新的行即可。最后,记得在 init.lua 文件里面加上如下内容,这样 lsp.lua 的配置才能被加载

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

重启 Nvim 之后,你应该可以在下面的状态栏看到 Mason 正在下载并安装前面我们指定的 LSP(注意此时不能关闭 Nvim),可以输入 :Mason 查看安装进度。在你等待安装的过程中,可以输入 g? 查看更多帮助信息了解如何使用 mason 插件

大功告成🎉🎉🎉

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

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

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