Neovim方案
受不了Vim转了VSCode,又叛逃Neovim了。
VScode,Vim和IDE:一些唠叨
在2023年年末,近段时间用的比较多的是VSCode。以前也曾使用Jetbrains家的IDE全家桶,用过Goland, Pycharm, IDEA, DataGrip等。 在前司工作的时候,因为公司的开发环境只有CentOS服务器,也用了好一阵Vim。后来决定在全部语言都转向VSCode开发,VSCode经过 这么多年的发展,已经非常完善了,稍稍查些资料就可以捣鼓出Python、C++和Golang的整套环境,丰富的插件生态,甚至Vim的操作 键位也能通过安装插件搬过来,启动速度上也快过那些笨重的IDE们。
对于不喜欢折腾的程序员,VSCode真的真的已经足够好了,如果写代码对你来说仅仅是一份工作,你只想用最短的时间,花最少的 精力把需求实现,那VSCode是你的不二选择,绝大部分的插件设计的非常友好,上手迅速,鼠标点点就好,需要动JSON配置文件也仅仅 是复制粘贴几行就好。
但是……喜欢折腾的程序员确实存在,还不少,Vim comes to rescue!
在非Vim用户的眼里,用Vim就是在自找麻烦:怪异的方向键hjkl、零存在感的鼠标、让人摸不着头脑的退出命令。但我觉得Vim的忠实粉丝, 是那一拨控制欲较强,对完美有点执拗,享受编程,追随所谓的编程的”Zen”(禅意)的人。折腾,实际上是在摸索更适合自己的编辑器 工具,每个人的Vim都不一样,插件不一样,设置不一样,快捷键不一样……千人千面,学习的过程很难,适应编辑模式很痛苦,摸索 自己用着舒服的配置需要很长的时间,陡峭的学习曲线,不低的上手难度。学习Vim,就像剑客在磨自己的剑,追求那人剑合一的境界, 这就是Vim的Zen。VSCode就像一把瑞士军刀,样样齐全,标准化,需要什么工具掰开就能用;Vim像一把武士刀,初试连挥舞起来都 很难,但越用越熟练,形成肌肉记忆之后,更快更准。Zen也许源自于那个计算机还是个黑框框的时代,“老一代”程序员们不需要考虑 多么炫酷的UI界面,他们只想把两只手尽量都放在键盘上,用最高的效率写好代码。
Expensify的CEO在一篇博客 Why we don’t hire .NET programmers 里写了一段对C#语言(.NET平台)的评价,作者把C#程序员比作麦当劳厨师,他拥有一套完整的、闭环的而且十分好用的做菜 系统(.NET平台),他能用最短时间做出世界上最美味的汉堡包,无论何时何地,汉堡包的质量都一丝不苟,始终如一。但是他没办法 做麦当劳菜单以外的菜,他甚至都不能在汉堡里放更多的肉,因为一切都是设置好的,他只需要按下适当的按钮,机器就把菜做好了, 机器上不会有一个用来放更多肉的按钮,这个厨师和这套自动化系统绑定在一起了。
当然,这篇文章的作者的看法有待商榷,我也无意批判.NET程序员或者Microsoft。除了麦当劳的比喻,博客作者也提到他想要招的
程序员:能用树枝串起剥了皮的松鼠在火上烤的“厨师”,一切都从零做起。Vim显然是更符合这样的哲学的,Everything from scratch,
往往一个很简单的需求配置,也需要你去了解一些Vim的规则,需要在.vimrc里写几行,甚至定义一个函数。就好像要自己生火、剥皮、
砍树枝、撒调料……这个过程是有意义的,他是有循序渐进的、螺旋向上的。你会越来越熟练,越来越熟悉Vim,知道能想到一些独创的
巧妙点子来。Vim鼓励你去思考自己的解决方法,去借鉴别人的解决方法。
有人会反驳:也许IDE没办法自己写代码解决问题,但VSCode和Vim不都是靠装一堆别人写的插件吗?对也不对,我敢保证如果你只会 Google一下,然后找一个插件装上,你百分之百无法用好Vim。Vim绝大多数的插件都不鼓励装上就什么都不管了,相反,热门插件 提供了相当多的配置项和自由度,鼓励用户微调到最顺手的状态。保持这种探索和精益求精的状态,我认为更好。
Neovim
言归正传,本文的主题是Neovim。什么是Neovim?
Neovim是Vim的一个重构版本,在2015年发行。它致力于解决Vim存在的问题,提供更现代化的API和特性。相比于Vim,它没有那么多 历史包袱,更加激进,支持LSP这样的新潮玩意。Neovim其实是Vim的超集,兼容Vim插件和脚本,编辑方式上也一脉相承。Neovim支持 语法更加友好的lua语言作为配置文件和编写插件,也支持Python等其他语言开发插件,这些都是Vim无法提供的。
Vim的作者Bram Moolenaar于2023年8月去世,他也是Vim的BDFL(Benevolent Dictator for Life),感谢他一生为Vim做出的贡献。
使用下来,我认为并不存在什么Vim令人难以忍受而Neovim解决了的痛点,Neovim更像是Vim的儿子而不是来革Vim的命的。如果一个 新手想入门Vim,我肯定推荐他一步到位直接用Neovim,除非有什么外在因素例如服务器上不允许安装外来软件之类的。同时,肯定也 有一大票Vim的“前朝遗老”不愿意走出舒适圈投入Neovim的怀抱,我觉得这一点问题都没有,毕竟我前司的服务器上只有个原装的Vim +几个老掉牙的插件,不影响几百号人每天用这套东西写8个小时代码……
脚手架配置(Scaffolding)
跟Vim一样,我没有选择从零开始手写一份配置入门Neovim。我用了这个项目作为基础, 我fork了一份([地址](https://github.com/yangrq1018/nvim-config),做了很多客制化修改。
这个项目本身就是一个开箱即用的neovim配置,克隆到~/.config/,重命名为nvim,启动Neovim自动安装全部插件,
大概有70多个插件,所以耐心等待一会。
除此之外,有一些可选安装。
若要使用
wilder.nvim必须要安装nvim的pyth0n扩展,nvim除了lua之外支持Python等其他语言编写插件,比如wilder就是python 编写的,官方称这些插件为Remote Plugin,他们不能直接使用。Python插件需要pip install pynvim,然后在nvim里输入:UpdateRemotePlugin,成功的话会看见wilder.nvim在输出里,这一步实际上是生成了一份跟Python模块对应的vim script
vim language server必须安装,否则出现所以代码都没有高亮之类的问题。方法:
npm install -g vim-language-serverlua LSP和python LSP非必要安装,但我建议装上,因为他们都不复杂。如果你的python是Anaconda的话pylsp可能已经 预装了。
系统上必须至少存在一个C编译器,clang或gcc,部分插件需要编译。
若要使用
xmake和dropbar,neovim版本需要0.10.0或者更高。目前正式版还没有发布0.10.0,所以需要下载nightly版本。
自定义修改
我的fork配置项目在原项目基础上做了这些修改
- 强制启用treesitter,因为有太多插件需要这个东西
- onedarkpro的lualine inactive window背景 当分屏多个窗口时,lualine提供的statusline会分割各个窗口。onedarkpro这个主题设置inactive的窗口的 statusline是跟背景一样颜色,很不显眼,导致很难看清窗口之间的分解,其他的主题就没有这个问题。 我修改了主题里的背景颜色,使其颜色更深,看着会比较醒目
after/ftplugin里,我修改了一些语言的折叠选项,- 修改终端标签的title
- 增加Jupyter notebook文件识别
- 显示bufferline文件类型图标
- 处理dap-virtual-text的颜色问题,由于colorscheme设置,
NvimDapVirtualText这个highlight group需要强制link到Comment,否则无法和comment保持一样的颜色
增加了一个实用命令
1 | " close buffer without quiting |
这个命令解决的是Vim一个很微妙的问题:无法关闭当前focus的buffer。如果用:bd命令关闭buffer,Vim会认为这个window没有
buffer了,如果当前仅有这一个window,那Vim就退出了。这导致一个很棘手的情况:无法清理工作区。如果当前项目打开了很多个
文件,这是想要关掉不用的buffer,第一个方案是用bufferline的功能,bufferline支持点击页签上的X按钮关闭buffer,但这需要
用鼠标,显然是跟Vim哲学背道而驰的。第二个方案是用:bd命令,但由于前述原因,:bd会导致Vim退出,从而整个工作区的
状态都丢失了,这往往是相当恼人的。所以,我们新定义一个:Bd命令来代替:bd,bp首先切换到上一个buffer,然后bd#
关闭最近打开的buffer,也就是我们想要关闭的那个,这样Vim就不会退出了。
C++环境,Xmake和LSP
Neovim做C++开发,可以利用Neovim的LSP支持。我使用的编译工具是Xmake,Xmake本身就提供了生成项目的compile_commands.json
文件,clangd作为LSP可以读取这个文件夹,提供给Neovim代码跳转、高亮、警告、报错等功能。这样,Neovim就变成了一个满血
IDE。借助LSP,Neovim和VSCode在功能上已经可以平分秋色了,可能Debug上还差一点,但体验已经很丝滑了。
Lazy.nvim的插件配置
1 | -- Xmake build tool |
这个插件会自动检测项目内的xmake.lua文件,文件内容变化时自动生成/更新compile_commands.json。clangd如果没有的话
要自己安装下,我只推荐Linux用户去折腾这个,msvc就不要去弄了,clang/llvm在Windows上还没有可用性。
创建一个.clangd文件,配置该项目的clangd服务
1 | CompileFlags: |
此文件是非必须的。默认情况下clangd会在当前编辑文件的父级目录,或者各父级目录下的
build/目录下查找 名为compile_commands.json的文件。上面例子中,配置CompilationDatabase指示clangd在.vscode文件 夹下寻找compile_commands.json,neovim插件调用xmake生成的数据库在这个路径下。为什么?因为vscode通 常会把文件放在这个目录下,为了方便别人用vscode打开项目,所以就统一放在这里了。
Add属性是可选的,如果打开cpp文件不会显示标准库<iostream>之类的找不到的报错,那就不用加,这里是指示clang在这些
路径搜索标准库头文件。
几句题外话,如果你看过compile_commands.json的内容,你会发现有点怪:里面的编译指令用的是gcc,而不是clang。xmake
在Linux下的默认工具链是gcc,除非你手动xmake f --toolchain=clang切换工具链为clang或者llvm。:LspLog查看clangd
的log,发现clangd确实是调用gcc编译,产生的index也是基于gcc生成的object code,这整个过程里都没有clang编译器什么事
……遗憾的是,这种兼容性在Windows平台上不存在,clangd无法兼容MSVC,所以我刚才说Windows用户就不要折腾clangd了。
虽然经过简单设置,可以在lualine里显示Xmake的状态,比如当前target,当前模式等等。statusline甚至支持点击弹出菜单,提供
类似VSCode的交互效果。不过,鼠标操作并不是Vim用户中意的方式,设置keymap才是。借助which-key插件设置Xmake命令的快捷键
1 | -- keymap |
可以看到,which-key插件的特色就是传入一个Lua table,定义多层的快捷键。我们使Xmake相关的命令以<leader>键开头,然后
是x键,然后才是各命令对应的按键。这样层层组织很方便我们记忆,以及将各个插件的命令和功能有条理地在键盘上组织起来。
meson/ninja
上面介绍了xmake作为build system来管理的C/C++项目,但不得不承认,xmake还是一个不算很主流的构建工具, 尤其是在国际上。目前大部分开源社区的C++项目还是使用CMake,或者更新一些的会使用Python编写的Meson。关 于CMake,由于过多的历史问题,其怪异的语法,非常糟糕的官方文档,都使得很多简单的问题复杂化了。所以我 的意见是积极地脱离这些过时的工具,当然是在条件允许的前提下,例如一些新项目上。
整合Meson和Neovim的LSP框架的思路,跟XMake相似,都是利用clangd作为LSP server提供源文件的解析。那么剩
下的问题就是,clangd只需要读取一个编译指令文件compile_commands.json,如何用Meson生产项目的这个文件
数据库呢?
1 | meson build |
First, meson build (equivalent to meson setup build) configures the project and generates a
build system under build/ directory. ninja -C build actually builds the C++ project, generating
the intermediate files and executables in build/.
So far this is just the normal procedure using Meson to build a project, but that’s not what we
want. To generate a compile database, go to the build directory. Ninja has a tool called compdb
for generating that. The compdb tool accepts a set of rules, which can help us limit what kind of
commands will be output. If you provide none of them, you could get commands to generate things that
are not related to C/C++ targets. Consider passing c_COMPILER and cpp_COMPILER rules to make the
database smaller.
Run
ninja -t rulesto get a list of available rules.
The database is generated at build/compile_commands.json. You can cd .. to the source directory
and start editing the source files using neovim or an editor you like. clangd will automatically
pick up the database file at $SRC/compile_commands.json or $SRC/build/compile_commands.json.
Should you changed meson.build, run ninja reconfigure to update the database, clangd should be
able to pick up the changes to the file and reindex all the files.
Git workflow
Blame with vim-fugitive.
vim-fugitive is a popular plugin to integrate version control (Git) into (Neo)Vim. It was
originally written for Vim, but compatible for Neovim, definitely. “Fugitive” means “escaping
prisoners”, that is, the plugin is so nice that it should be illegal. :D
This is a huge and comprehensive plugin, with a lot of functionalities related to git packed up. A
common need for developers is git blame. vim-fugitive makes blaming possible inside Vim while
editing a file. Use :Git blame command, it opens up a side menu on the left. <ENTER> or press
o on a commit to open it up in a new buffer, where you can check the line diffs.
Blaming once might be enough for finding what the current line is doing. Sometimes you need to blame
on that line multiple times, a.k.a. reblame. You can do that with vim-fugitive. After opening up a
commit, put the cursor on the - line (the line got deleted in the commit that introduced the
current line, call it A), press <ENTER> will open up the blob in the last commit that contains the - line,
that is usually A~. Type <ENTER> again on that line to blame on it. Again, it opens up the
commit that introduced that line, an ancestral commit of A~. You finish reblaming.
In short, to reblame, go the previous line, press ENTER twice.
Debug in Neovim (C/C++/Python)
:Lazy显示我们已经在Neovim里安装了60多个插件了,这60多个插件提供了简洁而实用的界面、比肩VSCode的现代化编辑器功能,
以及Vim独特的键盘操作。Neovim可以完全胜任开发一个大型项目,有着几乎你需要的所有功能。然而,有一个功能还没有:Debug。
当然,如果你熟练使用命令行debugger如gdb、lldb,可以跳过此章节。
debug主要需要的东西是nvim-dap插件和c++语言的DAP adapter,如lldb-vscode。除此之外还有其他的选择,参考[文档]
(https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation)。lldb-vscode通常在LLVM中就附带,通常在
/usr/lib/llvm-17/bin/lldb-vscode(LLVM version 17)。配置dap.lua
1 | local dap = require('dap') |
我对program做了一些修改,通过读取xmake插件的可执行文件路径,自动确定lldb的参数,如果你不是使用xmake,用cmake,
或者其他语言(python,golang, …),这里可以自由修改。
然后自定义键位和断点图标
1 | local whichkey = require("which-key") |
Python的DAP设置类似
1 | dap.adapters.python = function(cb, config) |
也是先定义adapter再做configuration引用刚才定义的adapter。
VSCode like launch.json support in nvim-dap
Use VSCode launch.json format in project directory to define project-specific debug session.
Useful to specify program arguments, for example.
1 | dap_vscode = require('dap.ext.vscode') |
依赖安装
上面用到的clang-format,clang, clangd, lldb, lldb-vscode这些工具属于llvm工具链。
很可惜,apt只提供了一些比较老的版本,最高应该是到15,如果你希望用更新的llvm,或者你的包管理器根本查不到llvm,
参考llvm官网添加源。
信任llvm的gpg签名
1 | # 方法1 |
如果你的Ubuntu版本比较新的话不要用方法一,不然apt update的时候会弹警告。
创建/etc/apt/sources.list.d/llvm-apt.list,找到你的OS版本和llvm版本对应的行粘贴上,比如笔者是22.04(jammy)和llvm 17,
1 | # 17 |
然后apt update更新索引,apt install clangd-17
在archlinux上则简单许多
pacman -S clang llvm lldb
Golang LSP
安装gopls,Golang官方提供的LSP实现。
1 | go install golang.org/x/tools/gopls@latest |
设置lspconfig,
1 | lspconfig.gopls.setup({}) |
Formatter
C++需要安装clang-format,Ubuntu可以通过apt安装
1 | sudo apt install clang-format-17 |
如果你对版本没有要求,可以忽略版本号。因为笔者的llvm环境是版本17,所以选择安装一致的clang-format版本。
在vim script里配置Neoformat插件
1 | let g:neoformat_enabled_python = ['black', 'yapf'] |
这里配置了Python, C/C++, Lua的formatter,分别使用clang-format, lua-format和black或者yapf。
用:Neoformat格式化当前文件。Neoformat仅仅提供Neovim的对接,我们可以自由选择不同语言使用的formatter。
Golang的格式化:gofmt是标准库的组件,无需额外安装,对golang filetype配置即可。
Edit 20240118: 切换为
mhartington/formatter.nvim,请使用这个插件,支持lua脚本配置。
语法高亮
nvim-treesitter插件提供了构建语法树,对多种文件格式进行语法高亮的功能。
这个插件比较复杂,它不光提供了语法高亮功能,以不同颜色标注出函数,参数,变量和关键字等信息,其他插件也可能依赖
nvim-treesitter的功能。比如nvim-dap-virtual-text这个插件,功能是在代码中实时显示dap调试的变量值,在文档里提到了插件
依靠nvim-treesitter来定位变量声明。
其实,发现这个问题耗费了我不少时间,我在安装dap这组插件的时候碰到奇怪的问题:插件没有任何报错,但调试的时候变量值
并不会显示在代码里。我起初怀疑是vim virtual text的问题,nvim-dap-virtual-text这个插件是通过virtual text把变量值
“插进”代码中的,所以我怀疑是我安装的Github Copilot也是通过virtual text显示suggestion,和dap的插件产生了冲突。然而
即使我卸载了Github Copilot,问题仍然存在。结果是nvim-config脚手架闹的乌龙,
1 | { |
diff显示,treesitter在linux系统根本就没有安装,所以导致了无法显示调试变量值。
另外,安装treesitter之后你可能会遇到打开一些格式的文件一片错误堆栈糊在脸上。比如:h显示的vimdoc,
可能会显示有node无法识别。这种情况大概是没有安装对应语言的parser,treesitter也就无法正确解析语法树。解决方法可以是
在nvim-treesitter的setup里配置
1 | require("nvim-treesitter.configs").setup { |
这里我加上了vimdoc等几个parser。或者:TSInstall命令安装,如:TSInstall bash会安装bash的parser。
最后,可以:TSInstallInfo查看已经安装了哪些parser。
事实上Neovim自带了一些parser,一般在
/usr/lib/nvim/parser下,而treesitter自己的parser在~/.local/share/nvim/lazy/nvim-treesitter/parser,是一些so文件,比如markdown.so,python.so。 treesitter仓库的issue里提到了treesitter的parser有高于 neovim parser的优先级。遇到无法识别node的错误,是没有安装treesitter的parser,从而 fallback到neovim的parser,而这个自带的parser产生了错误。
输入法自动切换
这是一个中文等非拉丁文语系的用户的痛点:输入法。众所周知Vim的操作是基于ASCII的,hjkl方向键,输入的是hjkl的英文ASCII 代码。但遇到编辑中文Markdown文档的情况,在INSERT模式我们要用中文输入法去输入,NORMAL模式要用英文键盘去操作,就会很麻烦。 每次切换模式都要切换一次输入法,非常影响效率。那么有没有办法在NORMAL模式自动切换到英文输入法,在INSERT模式自动切换到 中文输入法呢?
- Windows, WSL, MacOS兼容方案
im-select提供了跨平台的在终端切换输入法(IM)的功能。我看了下源码,作者是
通过C++调用各平台的API实现的,在Windows下是调用win32的GetKeyboardLayout获取窗口句柄的输入法代码,然后PostMessage
发送WM_INPUTLANGCHANGEREQUEST消息给窗口切换输入法。
我的使用场景是在Windows Terminal里用WSL里的Neovim,所以我把im-select.exe拷贝到Windows系统的Path,然后安装nvim插件,
下面展示了lazy.nvim插件实例
1 | { |
这里依靠的是WSL的一个特性:可以在WSL的终端里调用exe程序,甚至WSL的Path里就包含了宿主系统的Path。
解释一下原理,1033是Windows的英文输入法,设置为default,配置离开Insert模式时切换到英文,进入Insert模式时切换到上一 次离开Insert模式的源输入法(从输入法切换为default (English),X就是previous输入法)。所以我们只需要在第一次进入Insert模式后 切换到中文,比如搜狗输入法,然后按Esc,自动切换为英文,可以使用vim按键,再按i,自动切换为中文,直接打字中文内容。
插件的实现也并不复杂,核心代码是下面的两个函数,restore_default_im记住之前的输入法代码,切换到default;
restore_previous_im切换到之前保存的输入法。
1 | local function restore_default_im() |
这个插件很好的展示了lua语言编写的Neovim插件的可读性非常好,明显强过Vim的插件。这个优势非常重要,前文我们说过了Vim插件 是需要用户参与到配置中来的,还可以更进一步,用户应该参与到插件的开发中。插件不可能十全十美,总有这样那样的bug,也无法 满足每个人的需求,那么一个代码浅显易懂的插件,就极大地方便了我去fork下来然后调整它
- Linux方案
在Linux, 我们不需要用
im-select.exe这样的程序来切换输入法。Linux的IME大多都支持在命令行切换输入法,这里我们以fcitx5为例展示,笔者在Ubuntu 22.04上也是使用这个中文输入法,这可能是22.04版本唯一可用的中文输入法。实际上fcitx5的安装还是 有一些难度的,使用起来也容易遇到一些小bug,在另一个博客会详细解释。
我们不借助任何插件,在lua脚本里实现,来源
1 | local switch = { |
思路很简单
fcitx5-remote可以获取当前输入法代码,以及切换输入法- autocmd在
InsertEnter和InsertLeave执行函数,N模式和I模式分别用英文和中文输入 treesitter帮助判断markdown里的代码快,不切换输入法- 因为大部分切换代码的需求在markdown文件,所以对代码文件和markdown文件分别做匹配执行逻辑
因为这个方案是针对linux的,在init.lua里有选择性地加载该模块
1 | if os_name == "Linux" then |
剪切板
把nvim和系统剪切板整合起来,至少安装一个window system的剪切板工具(x or wayland)
apt install xclip
系统剪切板的内容存在寄存器+和*里。
Colorizer
Use nvchat fork
Crucial commit: 778cdd6 feat: Incremental highlight loading
Neovim + Tmux + Alacritty True Color
See Gist
Known bugs and issues
wilder.nvim启用后,bufferline无法及时同步通过:bd命令关闭的buffer,只有触发切换buffer或者mode时选项卡才会消失wilder.nvimPython扩展在Windows上无法启用(无法识别为RemotePlugin加载),在WSL环境中正常Dropbar can’t align with git blame: modify enable to whitelist fugitive-blame files
Wayland clipboard unnamedplus bug, create a window to copy paste so flicker title
gopls cmp selection request, how to disable it
conda env pylsp and python host missing issue