(Neo)Vim 是你的下一个文章写作编辑器(放弃用它写代码吧)

我曾经和所有人一样,觉得 (Neo)Vim 是终极软件工程师唯一指定的代码编辑器,是迈入极客圈子的门票。但在我认真读过 Vim 入门文档教程之后,一个异端想法开始一直在脑子里挥之不去:那就是写 LaTeX、Markdown 等文章才是它们在21世纪的最终归宿。经过博士四年的亲身实践,我现在更是可以更加自信地说,至少在 LaTeX 英文论文写作当中,(Neo)Vim 真的是我强烈建议每一个用户去尝试的编辑器。

光标移动(Motion)

光标移动是 Vim 用户永远的第一课,但我一直觉得很多原生的光标移动动作(Motion)都是为了自然语言文本而设定的。如果说以词为界限的动作——w(下一个词头)、e(下一个词尾)、b(上一个词头)这些——尚可适用于程序代码,那像 ) 这种则完全和编程没有关系了:它指的是跳到下一句话的开头!“下一句话”是怎么定义的呢?是以句号、问号、感叹号等常规文法中的标点符号再加空格所隔开的内容。再进一步,我们还有 },走到下一个段落的开头,而段落是由空行隔开的内容。]] 则是走到下一个章节的内容,章节标记就主要是在设置项里匹配确定的了。

在编程中,我们根本没有句子、段落、章节的划分,这些键位就自然没有用了。即便编程当中我们会用空行隔开逻辑上不太相关的代码段,但是空行可以出现在任何逻辑层级(比如函数定义内和函数定义体之后),所以以空行为单位的光标移动,在编程场景下仍旧不是最佳方案。而在自然语言文书中,以空行分段可以说是西文中非常常见的手段。而在 LaTeX、Markdown 这些文本标记语言从西方走向世界之后,空行分段的习俗也广泛传播到了中日韩等语言的数字文档中。(并且即便传统中文强调段前顶头空两格标志分段,在基于 Word 等现代软件的电子文档中,也经常见到段间有额外垂直空间。)不过其中有一个局限是,Vim 中句子的定义是严格按照指定的拉丁标点符号分的,也就是说在中日韩写作场景下不太适用。

文本对象(Text Objects)

Vim 光标移动的必修第二课就是文本对象:在配置好匹配规则的情况下,可以用快捷键指定一串文本片段,再进一步操作它内部的指定部件。比如 (现在有一串被括号包起来的话),我们可以把光标放到括号上或括号内,然后用 di( 来达到“删除(delete)在当前括号(()之内(inside)的内容”的目的。这一套组合技最大的特点是,我真的在讲自然语言,把脑子里想的英文指令描述给缩写打了出来。

前面的动词可以是删除(d)、删除并进入文本输入模式(change)、复制(yank)、选中并进入框选模式(visual)等。

中间表示范围的词,原生有 inside 和 around 两种,例如 i( 表示仅括号内全文,a( 同时也包含括号本身。第三插件往往会用到 surround,比如 s( 表示仅仅左右括号两个字符。

最后的范围原生支持词(w)、句(s)、段(p)、各种括号(([{<)、和各种引号("'、```)。

对于文本标记型语言,格式(例如加粗、斜体)往往是作为轻量级的代码指令包裹在正文文本两边的,Vim 的文本对象机制正好擅长处理它们。比如对于各种复杂的 LaTeX 指令,地表最强的 vimtex 插件会帮我们额外定义很多文本对象。当我把光标放在 \textbf{一些加粗文本} 的任意地方时,都可以用 _ic_ac_sc 来操作“LaTeX 命令(command)的花括号参数内”、“整条命令”、以及“textbf 这个命令名本身”。例如在实战中,csctextit 就可以把光标所处的加粗文本改成加斜。除了 LaTeX 命令(c)之外,vimtex 还支持 LaTeX 环境(environment,即 \begin{xxx}...\end{xxx}),数学公式($),段落(P)和列表项目(m

vimtex 插件的其他功能

这一块我不想讲太深入,不然就是对 vimtex 文档的机械重复了。我只想简单提其中几个我觉得尤其好用的功能,比如把光标当前行之外的所有引用、数学公式、加粗加斜等命令渲染成最终的样式,在行内数学公式(inline math)和块级数学公式(display math)之间一键切换,一键把 () 或其他括号转换成 \left(\right) 的自动调高版本,一键补全当前 \begin{xxx} 对应的 \end,以及一键给选中的文本包一个命令等等。这些都不是只 Vim 引擎才能做到的功能,但是为什么其他软件的 LaTeX 插件就做不到呢?

如果想要论证 (Neo)Vim 相比其他文本编辑器更方便写文章,那么以上三点就足以说明情况了。其实我在写文章时使用最频繁的,恰恰是 vim 最基础的光标移动和文本对象功能,并且它们作为 vim 设计哲学的核心板块,是其他代码编辑器完全无法代替的。即便是 VSCode 加 Vim 插件、亦或是 Zed 这样的新生代仿 Vim 编辑器,也会因为这两个核心板块的不可拓展性而败给 (Neo)Vim 加 vimtex。

那么接下来第二个批判的议题是——

(Neo)Vim 反而不一定适合写代码

心智模型

当我熟悉在 NeoVim 里面写 LaTeX 之后,我发现它在思维上也非常的符合直觉。我们不管是写文章还是写代码,写作的时候脑子里想的是这个文本作品的逻辑结构,例如“下一句‘话’怎么写”、“上一个‘函数’是怎么定义的”。而当我们移动光标以满足写作需求的时候,我们需要转换到视觉层面的操纵。在自然语言中,我们文本的逻辑结构已经反映在视觉线索上了:段落就是空行,章节开头就是有特定格式写的章节标题。而在编程中,逻辑结构在视觉线索中的映射相对不明显:定义一个很重要的全局变量和一个无关紧要的局部变量都是用同样的关键词打头,以及 letfunction 这些标记关键词相较于庞大的定义体实在是难以一眼就看到。

甚至呢,自然文本中的前后文本身就有很强的逻辑联系,相邻的两句话不可能毫无关系,所以 )( 这样的整句跳跃也是合乎情理的。但是编程的两个逻辑块,比如两个相邻的函数定义,很有可能是毫无关联的,或者就算有一定的相似性,我们也很少有按顺序阅读它们的需求,而更多是跳转到各自被调用的地方。

我记得在抱着编程的心态学 Vim 的时候,看到几乎所有人都需要花很多时间训练自己熟悉 Vim 的光标移动和文本对象概念,包括我自己也是其中一员。现在回想起来,可能这就恰恰反映了编程时截然不同的心智模型。而后我又想到,我在 VSCode 中定位需要编辑的代码片段时,其实心里大概想的是一个视觉图案,比如“大概在文件 2/3 位置、前后很多空行、红色很多蓝色很少、前后都很短但是就其中一两行很长的地方”。进而定位时经常会参考滚动条侧边的缩略图,或者往下猛然滚动之后扫到一个印象中的视觉图案。这也反映出了同样的问题。

再更近一步说,我甚至认为 Vim 这种终端年代设计出的软件产品,长文的翻页和滚动恰恰有非常大的弱势。不仅是 Vim,请也回想一下在终端用到 man、less、甚至 tmux 的经历。终端界面(TUI)仿佛在设计之初就不考虑滚动条,如果有的话也只是在底部的状态栏里的一个百分比。 习惯了图形界面(GUI)之后,我往往会觉得 TUI 中滚动长内容的过程非常没有安全感。 翻页的时候整个视窗直接跳跃到新的地方,没有一个“划过”的感觉、也没有对进度的实时感知

繁琐的配置

编程语言的往往涉及到复杂、结构化的语法,乱七八糟的开发调试工具链,以及一个项目中多种语言混合的情况,所以 Vim 的配置也会随着开发需求的变化而爆炸增长。尤其是在近几年里,社区探索出了越来越多的外围概念,比如 TreeSitter 为每一个语言定义抽象语法树来解决语法高亮、代码折叠等需求,LSP 为不同语言的代码操作(补全、格式化、重构等)提供统一接口,DAP 为不同语言的调试运行(打断点等)提供统一接口……配置 (Neo)Vim 的过程往往演化成协调上述接口层和每个语言专属工具的无底深渊。

纯文本编辑在这方面的压力就小了很多。首先是它们基于自然文本的呈现方式,注定了不需要 TreeSitter 抽象语法树也能用 Vim 引擎自带的正则表达式匹配法来完美理解代码结构。它们也不存在重构代码等复杂操作的需求,更没有调试运行的需求。一个基于传统正则表达式实现的 vimtex 插件不仅足矣完成 LaTeX 的写作需求,更是提供该需求的最佳解决思路。我早期也尝试过更新潮的 TreeSitter 和 LSP 解决方案,发现它们在功能完备程度上甚至比不过 vimtex。

另外 LSP 接口层之于 LaTeX 还有一个很尴尬的局面。它采用大多编程语言的命名习俗,在快捷键配置中采用 function 和 cclass 指代 LaTeX 中的命令(command)和环境(environment)。这直接导致我光标移动和文本对象的指令是反直觉的,无法和自然语言对应的。

代码编辑器设计挑战

一个当代合格的代码编辑器,尚有很多设计考量在文本编辑区之外。比如文件列表、分屏、临时终端等等。这些都在挑战 Vim 作为一个纯粹代码编辑器的初始设计哲学,并且它们在 Vim 中的很多实现也都是在底层魔改了文本编辑窗口而做出来的。这样魔改出来的 Vim 多少会水土不服,比如我的 NeoVim 在尝试记忆我之前会话打开了哪些窗口的时候,会把我文件列表窗口也误当成一页。

在 AI 时代,更多挑战出现了。AI 会在你一边写的时候一边在后面做联想补全,AI 允许你框出来一段话再对它进行问答和修改,AI 会自动识别可能有问题的代码行并高亮他们,AI 会在你改了一处代码后自动识别另一处需要连带修改的代码,AI 还能以聊天机器人的模态在侧边常驻……所有这些功能都要求代码编辑器从纯粹的文本编辑器向完备的 GUI 转型,而任何试图在 TUI 中复现它们的努力,最终都会吃力不讨好、也缺乏前瞻性。

我的样例配置

我自己的配置是 NeoVim 程序、加 LazyVim 基础配置、再加自己 vimtex 微调。NeoVim 是一个重写版的 Vim 超集,LazyVim 是一个基于 NeoVim(而非原版 Vim)的基础配置集,涵盖了文件列表、文件查找、全局搜索替换等等基础插件的配置起点。同时因为很多现代 (Neo)Vim 配置都让 TreeSitter 和 LSP 等借口层自动接管所有编程语言,我自己的额外配置中禁止他们接管 LaTeX 文件类型。配置全内容可以在我的GitHub仓库中看到。

Hugo 驱动
基于 JimmyStack 主题设计