用 Nvim 打造超强的个性化 IDE 你知道世界上最简短的悼词吗?它只有 3 个字符,却让全世界的程序员为之悼念。
写在前面 我最早接触 vim 是在约一年半以前,那时我刚转到 linux 环境下不久。还记得最开始和 vim 打交道的时候,连怎么退出都不会,只知道在键盘上乱按:Esc,Ctrl+C,F4 …… 后来,随着我对 vim 的了解愈发深入,“配置一个专属自己的个性化 IDE,逃离 Vscode 魔爪”,成为了我的一大乐趣。
当时,我照着 B 站 up 主 TheCW 的视频依葫芦画瓢地配置了一个及其简陋的版本,不但功能欠缺,还经常出错。后来,我逐渐完善我的 vim 编辑器,添加了许多插件,还逐一配置了各种语言的 LSP 服务。但是,随着时间的推移,我的 vimrc 文件变得愈发臃肿和难读,还时不时需要解决不知道怎么冒出的问题。
因此,我决定转移到 neovim。neovim 完全继承了 vim 的全部优点:轻量、高效、可定制。而另一方面,它提供了更成熟的基于 lua 语言的配置框架,使你能够以更加结构化的方式对你的配置文件进行管理,而不是在 vimrc 里面“一锅乱炖”。
下定决心后,我开始着手 neovim 的配置。配置的总体流程相对顺利,没有出现特别严重的问题。配置好的 neovim 在各个方面已比较成熟,运行时也没有发生严重的报错。于是,趁热打铁地,我决定对此次 neovim 的配置流程做一个记录,以便日后参考。
配置好后的 nvim 效果图最终如下图所示。
安装 由于我是 ArchLinux 用户,安装 neovim 并不是一件难事,Arch 官方提供了维护良好的软件包,可以通过包管理器 pacman 下载。
下载好后在终端输入 nvim 即可使用,与 vim 的使用方式一样。
主文件 nvim 的配置通过 .lua 文件而非 .vim 文件实现。前面说了,这是 nvim 相较于 vim 在配置管理方面的优点。
nvim 的所有配置文件均被存储在 ~/.config/nvim/ 文件夹下。如无特殊情况,后文的 “./“ 均指该文件夹。
在该文件夹下,创建一个 init.lua 文件。该文件相当于入口文件,nvim 启动时会先启动该文件。
1 2 3 4 5 6 require "core.options" require "core.keymaps" require "core.lazynvim"
lua 下的 require 相当于 c/c++ 中的 include 操作,会将模块内包含的内容全部导入。
在这里它导入了 3 个文件:
./core/options.lua
./core/keymaps.lua
./core/lazynvim.lua
前两个文件包含了 nvim 的基本设置和一些键盘映射,第三个文件则与插件管理有关,将在后面的章节具体介绍。
基本配置 通用选项 这里配置的是 nvim 原生的一些功能,包括:行号、缩进、光标行、剪切板等,内容比较杂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 local opt = vim.opt opt.relativenumber = true opt.number = true opt.tabstop = 2 opt.shiftwidth = 2 opt.expandtab = true opt.wrap = false opt.cursorline = true opt.mouse:append("a" ) opt.clipboard:append("unnamedplus" ) opt.splitright = true opt.splitbelow = true opt.ignorecase = true opt.smartcase = true opt.termguicolors = true opt.signcolumn = "yes"
这些选项的具体含义可以通过 :help vim 在 vim 的文档中查到。
具体来说,这里修改了某些变量的值从而使 nvim 的表现与原始状态不同。
键盘映射 键盘映射部分相对繁琐,由于我本人习惯使用 colemak 键盘布局,所以主要配置也是围绕这一方面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 vim.g.mapleader = " " local keymap = vim.keymap keymap.set("" , "u" , "k" ) keymap.set("" , "e" , "j" ) keymap.set("" , "n" , "h" ) keymap.set("" , "i" , "l" ) keymap.set("" , "s" , ":<CR>" ) keymap.set("" , "r" , ":<CR>" ) keymap.set("v" , "U" , ":m '<-2<CR>gv=gv" ) keymap.set("v" , "E" , ":m '>+1<CR>gv=gv" ) keymap.set("n" , "<leader>sv" , "<C-w>v" ) keymap.set("n" , "<leader>sh" , "<C-w>s" ) keymap.set("n" , "k" , "i" ) keymap.set("n" , "K" , "I" ) keymap.set("n" , "U" , "5k" ) keymap.set("n" , "E" , "5j" ) keymap.set("n" , "N" , "0" ) keymap.set("n" , "I" , "$" ) keymap.set("n" , "l" , "u" ) keymap.set("n" , "j" , "<C-r>" ) keymap.set("n" , "S" , ":w<CR>" ) keymap.set("n" , "Q" , ":q<CR>" ) keymap.set("n" , "<leader><CR>" , ":nohl<CR>" )
其中除了基本的光标移动外,比较实用的配置是 visual 模式下的单行或者多行移动,以及撤销、恢复、保存、退出的快捷键。
这里也可以看到 nvim 中的按键映射方式,是通过 vim.keymap.set() 函数做到的。
在这个函数中,第一个参数表示在什么模式下进行映射,”n” 表示 normal 模式,”v” 表示 visual 模式,”” 表示 normal/visual/operator 三个模式,也就是通用模式。
第二个参数表示被选中的按键,第三个参数表示你要将被选中的按键映射成别的什么按键或者什么命令。
插件 管理器与管理方式 lazy.nvim 成熟、稳定的插件管理器是必须的。
这里我选用了 lazy.nvim 插件管理器,号称目前最稳定的插件管理器。
同时,它的名字 lazy.nvim 与我的名字 lazypool 接近,给我一种莫名的亲切感。
使用插件管理器需要引导文件,也就是我们之前提到的 lazynvim.lua 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local lazypath = vim.fn.stdpath("data" ) .. "/lazy/lazy.nvim" if not vim.loop.fs_stat(lazypath) then vim.fn.system { "git" , "clone" , "--filter=blob:none" , "https://github.com/folke/lazy.nvim.git" , "--branch=stable" , lazypath, }end vim.opt.rtp:prepend(lazypath)require ("lazy" ).setup("plugins" )
我们来仔细查看这个引导文件,它首先声明了一个本地变量 lazypath,这是 lazy.nvim 管理插件的路径,默认是在 ~/.local/share/nvim/lazy/lazy.nvim。
这里的 vim.fn.stdpath(“data”) 其实就是 ~/.local/share/,如果在 windows 下那就是 %APPDATA% 环境变量所指的文件夹。
之后,它将 github 上的 lazy.nvim.git 这个代码仓库 git clone 到了 lazypath 中。
没错,lazy.nvim 通过 git 管理各个插件,也就是说,git 是这个插件管理器的依赖项!使用该管理器首先需要确保拥有 git 工具。
最后,通过 vim.opt.rtp:prepend(lazypath) 将这个 lazypath 添加到 path 中。
这样做实际上将 lazypath 下的所有文件夹都变成了可以在程序中 require 的模块。
事实上,所谓的插件,只是一些 lua 程序罢了,它们通常可以通过 git clone 从 github 上抓取下来。
插件管理器的工作也仅仅只是将其克隆到特定的位置,并将这个特定的位置添加到环境变量中,并在必要的时候进行仓库的维护罢了。
在代码的最后一行,require(“lazy”) 调用了刚刚下载下来的 lazy.nvim.git 仓库里的模块,并使用 setup(“plugins”) 函数载入位于 ./lua/plugins 下的所有插件文件。
其中,plugins 下的所有文件都返回名为 Plugin Spec 的 table 字面值。关于什么是 Plugin Spec 可以查看 lazy.nvim 的 官方文档 。
在这里我们主要关注如下的形式:
1 2 3 4 5 6 7 return { "folke/which-key.nvim" , dependencies = { "folke/which-key.nvim" }, config = function () require "which-key" .setup {} end }
其中第一项是一个字符串,是你要下载的插件的 git 仓库的位置。
dependencies 对应一个列表,是该插件的依赖项,当管理器下载此插件时,会先下载依赖项。
config 对应一个函数,是插件加载完毕后要运行的函数,通常将有关插件的配置写在此处。
./lua/plugins 下的所有文件的返回值都形如上述结构,彼此独立,互不影响,因此高度可移植。
主题与状态栏 tokyonight.nvim tokyonight.nvim 是一个常用的主题插件。
1 2 3 4 5 6 return { "folke/tokyonight.nvim" , config = function () vim.cmd[[colorscheme tokyonight-moon]] end }
插件仓库是 folke/tokyonight.nvim,这没什么好说的。(不过 folke 这个作者真是个大牛,管理器用的人家的,主题也用的人家的)
插件加载完毕后运行 colorscheme tokyonight-moon 从而获得美美的主题外观。
lualine.nvim 配置完主题后可以配置状态栏,没什么用,主要是帅。
1 2 3 4 5 6 7 return { "nvim-lualine/lualine.nvim" , dependencies = { "nvim-tree/nvim-web-devicons" }, config = function () require ("lualine" ).setup() end }
这里指定了依赖项 nvim-tree/nvim-web-devicons 这样状态栏可以显示一些有趣的图标。该依赖项在后面还会用到。
注意:使用 nvim-web-devicons 一定要搭配能够显示图标的终端字体,比如 Hack Nerd Font 和 FiraCode Nerd Font 等。同时为了避免图标和文字重叠,建议先用等宽字体(Mono Font)。
文档树与窗口切换 nvim-tree.lua 文档树是一个 IDE 最基本的需求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 return { "nvim-tree/nvim-tree.lua" , dependencies = { "nvim-tree/nvim-web-devicons" }, config = function () vim.keymap.set("n" , "<leader>tt" , ":NvimTreeToggle<CR>" ) require "nvim-tree" .setup { on_attach = keymaps, } end }
这里配置相对较冗长,篇幅限制不在这里展开。
但可以聊一下如何对文档树进行按键配置。对于 vim 用户,按键等于是灵魂。
这里首先通过 vim.keymap.set() 将 <leader> + tt 作为打开/关闭文档树的快捷键。
之后在 setup() 的时候将 keymaps 传入给 on_attach 来修改键盘映射。
keymaps 是自定义的按键绑定函数,如何定义可以查看 官方文档 。
vim-tmux-navigator 用于在各个窗口间来回切换。
1 2 3 4 5 6 7 8 9 10 return { "christoomey/vim-tmux-navigator" , config = function () vim.keymap.set("n" , "<leader>u" , ":<C-u>TmuxNavigateUp<CR>" ) vim.keymap.set("n" , "<leader>e" , ":<C-u>TmuxNavigateDown<CR>" ) vim.keymap.set("n" , "<leader>n" , ":<C-u>TmuxNavigateLeft<CR>" ) vim.keymap.set("n" , "<leader>i" , ":<C-u>TmuxNavigateRight<CR>" ) end }
在设置中主要针对 colemak 布局对切换窗口的快捷键做了绑定。
语法高亮与彩色括号 nvim-treesitter 语法高亮需要针对代码构建语法树,这里我们需要 nvim-treesitter。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 return { "nvim-treesitter/nvim-treesitter" , dependencies = { "p00f/nvim-ts-rainbow" }, config = function () require "nvim-treesitter.configs" .setup { ensure_installed = { "bash" , "c" , "cpp" , "css" , "go" , "html" , "java" , "javascript" , "latex" , "lua" , "markdown" , "python" , "vim" }, highlight = { enable = true }, rainbow = { enable = true , extended_mode = true , max_file_lines = nil } } end }
首先,它有一个依赖项 p00f/nvim-ts-rainbow,这是启用彩色括号的必须项。
之后在配置过程中, 在 ensure_installed 里选择需要安装的语法包。这里挑了我最常用的 13 个,其中包括 go,c/c++ 和 python 等。
之后启用高亮和彩色括号,并做简单的设置。
注释与自动括号 写注释,没什么好说的。
1 2 3 4 5 6 return { "numToStr/Comment.nvim" , config = function () require "Comment" .setup {} end }
该插件能够让你通过 gcc 和 gc 来快速将一行或几行注释掉。
具体的按键操作可以查看 官方文档 。
nvim-autopairs 自动括号,当你输入括号时会帮你把下一半也输出来,并且还有 FastWrap 的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 return { "windwp/nvim-autopairs" , event = "InsertEnter" , config = function () require "nvim-autopairs" .setup { check_ts = true , ts_config = { lua = { "string" , "source" }, javascript = { "string" , "template_string" }, }, fast_wrap = { map = '<M-e>' , chars = { '{' , '[' , '(' , '"' , "'" }, pattern = [=[[%'%"%)%>%]%)%}%,]] =], end_key = '$' , keys = 'qwfpgjluyzxcvbkmarstdheio' , check_comma = true , highlight = 'Search' , highlight_grey='Comment' } } end }
以上配置代码参考自 官方文档 。
缓冲区、文件检索与 Git 提示 bufferline.nvim 在编辑器顶部创建 bufferline,以将缓冲区的情况可视化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 return { "akinsho/bufferline.nvim" , config = function () vim.keymap.set("n" , "<C-i>" , ":bnext<CR>" ) vim.keymap.set("n" , "<C-n>" , ":bprevious<CR>" ) vim.keymap.set("n" , "<C-e>" , ":bdelete<CR>" ) require "bufferline" .setup { options = { diagnostics = "nvim_lsp" , offsets = { filetype = "NvimTree" , text = "File Explorer" , highlight = "Directory" , text_align = "left" } }} end }
这里主要看按键配置,其余部分均参考自 官方文档 。
Ctrl + I 打开缓冲区的下一个文件;Ctrl + N 打开缓冲区的上一个文件;Ctrl + E 将当前文件从缓冲区移除。
telescope.nvim 鼎鼎大名的文件搜索插件,以下的配置也主要参考 官方文档 。
1 2 3 4 5 6 7 8 9 10 11 12 13 return { "nvim-telescope/telescope.nvim" , tag = "0.1.5" , dependencies = { "nvim-lua/plenary.nvim" }, config = function () local builtin = require "telescope.builtin" vim.keymap.set("n" , "<leader>ff" , builtin.find_files, {}) vim.keymap.set("n" , "<leader>fg" , builtin.live_grep, {}) vim.keymap.set("n" , "<leader>fb" , builtin.buffers, {}) vim.keymap.set("n" , "<leader>fh" , builtin.help_tags, {}) end }
这里最主要的两个按键配置:<leader> + ff 根据文件名查找文件;<leader> + fg 根据文件内容查找文件。
通常来说第一个功能一般没有问题,但是第二个问题容易遇到问题。
因为根据文件内容查找文件需要一个名为 rg(ripgrep) 的工具。
该工具位于 https://github.com/BurntSushi/ripgrep 你需要先将其下载。
gitsigns.nvim 针对有版本控制的文件,使用 gitsigns.nvim 可以使你在编程的时候注意到哪些地方做了修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 return { "lewis6991/gitsigns.nvim" , config = function () require "gitsigns" . setup { signs = { add = { text = '+' }, change = { text = '~' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' } }} end }
这里主要修改了他的提示符,比如添加的地方用加号表示。
LSP mason-lspconfig.lua 在 nvim 配置 lsp 服务要比在 vim 配置容易得多,很大的一个原因就是统一的 lsp 服务器管理工具 mason。
这个工具是由 williamboman 开发的,可以方便的下载、卸载 lsp 服务器,并提供了 ui 界面。
它包含两个工具:mason 和 mason-lspconfig。
其中,mason 提供 ui 界面和操作接口,mason-lspconfig 则具体实现 lsp 服务器的获取和下载,两者不能分开使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 return { "williamboman/mason-lspconfig.nvim" , dependencies = { "williamboman/mason.nvim" }, config = function () require "mason" .setup { ui = { icons = { package_installed = "✓" , package_pending = "➜" , package_uninstalled = "✗" } } } require ("mason-lspconfig" ).setup { ensure_installed = { "bashls" , "clangd" , "cssls" , "gopls" , "html" , "jdtls" , "tsserver" , "texlab" , "lua_ls" , "marksman" , "pyright" , "vimls" } } end }
它们依然能够以插件的方式为 lazy.nvim 管理调度。
在这里我们看到,将 mason 和 mason-lspconfig 导入后,在配置的函数中首先为 mason 设置了一些图标用以显示 ui 界面,之后导入 mason-lspconfig 包并下载指定的 lsp 服务器。
mason-lspconfig 支持的能够直接下载的 lsp 服务器有一个清单,可以在其 代码仓库 查看。
这里我挑选了常用语言的 lsp,与之前在 treesitter 选定的差不多一致。
nvim-cmp.lua mason 和 mason-lspconfig 只是将 lsp 服务器下载到了本地,要想利用 lsp 进行代码的自动补全(cmp)还需要进一步设置。
这里用到了 nvim-cmp.lua 这个插件。
该插件由 hrsh7th 开发。除了 nvim-cmp.lua,作者还开发了一套与 cmp 有关的插件,这些插件通常被结合在一起使用。
此外,自动补全还需要第三方的代码片段引擎,这里选用了 LuaSnip 系列的三个插件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 local check_backspace = function () local col = vim.fn.col "." - 1 return col == 0 or vim.fn.getline("." ):sub (col, col):match "%s" end return { "hrsh7th/nvim-cmp" , dependencies = { "hrsh7th/cmp-nvim-lsp" , "hrsh7th/cmp-path" , "L3MON4D3/LuaSnip" , "saadparwaiz1/cmp_luasnip" , "rafamadriz/friendly-snippets" }, config = function () local cmp_ok, cmp = pcall (require , "cmp" ) local luasnip_ok, luasnip = pcall (require , "luasnip" ) if not cmp_ok or not luasnip_ok then return end require "luasnip.loaders.from_vscode" .lazy_load() cmp.setup { snippet = { expand = function (args) require "luasnip" .lsp_expand(args.body) end , }, mapping = cmp.mapping.preset.insert { ['<C-b>' ] = cmp.mapping.scroll_docs(-4 ), ['<C-f>' ] = cmp.mapping.scroll_docs(4 ), ['<C-e>' ] = cmp.mapping.abort(), ['<CR>' ] = cmp.mapping.confirm({ select = true }), ["<Tab>" ] = cmp.mapping( function (fallback) if cmp.visible() then cmp.select_next_item() elseif luasnip.expandable() then luasnip.expand() elseif luasnip.expand_or_jumpable() then luasnip.expand_or_jump() elseif check_backspace() then fallback() else fallback() end end , { "i" , "s" }), ["<S-Tab>" ] = cmp.mapping( function (fallback) if cmp.visible() then cmp.select_prev_item() elseif luasnip.jumpable(-1 ) then luasnip.jump(-1 ) else fallback() end end , { "i" , "s" }) }, sources = cmp.config .sources({ { name = 'nvim_lsp' }, { name = 'luasnip' }, { name = 'path' } },{ { name = 'buffer' } }) } end }
在配置函数中,核心部分是 cmp.setup() 函数,里面主要传入了三个键值对。
snippet - 指定了以何种方式解析代码片段,此处使用了代码片段引擎
mapping - 进行代码提示的按键绑定
sources - 指定了代码补全可以调用的源文件
以上配置均可以在 nvim-cmp 的 代码仓库 找到。
lspconfig.lua 设置代码补全的最后一步,利用 nvim-lspconfig 注册需要补全功能的语言。
nvim-lspconfig 是 neovim 官方提供的 lsp 处理器。
由于 mason-lspconfig 已经将所需的 lsp 下载到本地,可以直接使用 require “lspconfig”.<LSP SERVER NAME> 来获取到相应的 lsp 服务器。
之后通过 setup() 函数传入 cmp_nvim_lsp 提供的的默认 capabilities 即可启动代码补全的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 return { "neovim/nvim-lspconfig" , dependencies = { "hrsh7th/cmp-nvim-lsp" }, config = function () local capabilities = require "cmp_nvim_lsp" .default_capabilities() require "lspconfig" .clangd.setup { capabilities = capabilities } require "lspconfig" .marksman.setup { capabilities = capabilities } require "lspconfig" .tsserver.setup { capabilities = capabilities } require "lspconfig" .texlab.setup { capabilities = capabilities } require "lspconfig" .lua_ls.setup { capabilities = capabilities } require "lspconfig" .pyright.setup { capabilities = capabilities }end }
结语 到此为止,一个相当不错的代码编辑器就已经完成了,他有好看的主题和语法高亮、好用的文档树和文件搜索以及代码补全功能。
但是,neovim 的潜力远不止如此。
目前为止其实都还只做了比较基础的配置,在实际使用中肯定还会产生各种各样的需求,而我们的“自制” IDE 也还需要在实际环境中慢慢迭代,逐渐进步。