查看原文
其他

Vim 激荡 30 年发展史

Joe Nelson CSDN 2019-10-30

众所周知,Vim 是从 vi 发展出来的一个文本编辑器。其拥有代码补全、编译及错误跳转等丰富的功能特性,在程序员群体中广受欢迎。

本文是作者 Joe Nelson 从头到尾阅读 Vim 用户手册以及追溯历史之后的一些心得。希望这些笔记能够帮助大家发现这款编辑器的核心功能,从而更加熟练地使用各个插件。

作者 | Joe Nelson

译者 | 弯月,责编 | 屠敏

出品 | CSDN(ID:CSDNnews)

以下为译文:

如果你想进一步了解Vim,那么我建议你入手一本纸质的用户手册和优秀的袖珍参考手册。我没有找到官方的Vim纸质手册,最后只好打印了这个PDF(https://begriffs.com/pdf/vim-user-manual.pdf)。为了方便查看Vim的命令列表,我建议你入手上图中的《vi and Vim Editors Pocket Reference》。


历史


Vi的诞生

Vi源自QED编辑器,距今已有五十多年的历史。其发展历程如下:

  • 1966年:伯克利分时系统的QED(“Quick EDitor”)

  • 1969年7月:登月(仅供参考)

  • 1969年8月:QED -> AT&T的ed

  • 1976年2月:ed ->玛丽王后大学的em(“Editor for Mortals”)

  • 1976年:em -> 加州大学伯克利分校的ex (“EXtended”)

  • 1977年10月:ex有了可视化模式,vi

阅读一下用户手册,你就会发现QED和ex之间有很多相似之处。这两个编辑器在指定和操作行范围时都采用了类似的语法。

QED、ed和em这类的编辑器都是为硬拷贝终端设计的,这些终端基本上就是带调制解调器的电动打字机。硬拷贝终端可以将系统输出打印到纸上。显然一旦打印完成,就无法更改输出,因此这种编辑过程需要包含用于更新和手动打印文本范围的命令。

到1976年的时候,ADM-3A等视频可视化终端出现了。Ex编辑器添加了一个“开放模式”,允许在可视化终端上进行行内编辑,还有一个可视化模式,可以在支持光标的终端上面利用屏幕进行编辑。这种可视模式(可以通过命令“vi”激活)可以在屏幕上显示部分文件的最新视图,同时还保留了屏幕底部的ex命令行。(趣事:在ADM-3A上,h、j、k、l键兼作方向键,所以vi选择这几个键作为光标移动只是为了保持一致而已。)

如果你想了解更多关于从ed到ex / vi的发展,可以阅读Bill Joy的这段采访(https://begriffs.com/pdf/unix-review-bill-joy.pdf),他在文中谈到了ex / vi的创建过程,以及一些令他失望的事情。

传统的vi实际上只是ex的另一种形式,它们的可执行文件是同一个,根据调用时的可执行文件名来决定启动ex模式还是vi模式。ex / vi对之前的版本进行了改进,只需很少的系统资源,就可以在有限的带宽下操作。而且该工具还支持于大多数系统,完全符合POSIX标准。

从vi到vim

作为ed的衍生物,ex / vi编辑器的版权属于AT&T。如果想在Unix以外的平台上使用vi,就必须重新编写不使用任何原始代码的克隆版本。

克隆版本有很多,下面列出了一部分:

  • nvi:1980年,4BSD版

  • calvin:1987年,DOS版

  • vile:1990年,DOS版

  • stevie:1987年,Atari ST版

  • elvis:1990年,Minix和386BSD版

  • vim:1991年,Amiga版

  • viper:1995年,Emacs版

  • elwin:1995年,Windows版

  • lemmy:2002年,Windows版

下面,我们来重点看一看中间的vim。Bram Moolenaar希望在Amiga上使用vi。于是,他从Atari移植了Stevie,并对其进行了改进。他给自己的这一版起名为“Vi IMitation”。有关完整的第一手资料,请参阅自由软件杂志对Bram的采访(https://begriffs.com/pdf/vim-interview.pdf)。

在版本1.22中,Vim被重新命名为“Vi IMproved”,它完全实现并且超越了vi的功能。以下是主流版本及其重要功能的发展历程:

  • 1991年11月2日,Vim 1.14:首次发布(Fred Fish disk #591)。

  • 1992年,Vim 1.22:移植到Unix。Vim开始与Vi并驾齐驱。

  • 1994年8月12日,Vim 3.0:支持多个缓冲区和窗口。

  • 1996年5月29日,Vim 4.0:图形用户界面(主要由Robert Webb提供)。

  • 1998年2月19日,Vim 5.0:语法着色/高亮显示。

  • 2001年9月26日,Vim 6.0:折叠,插件,垂直分割。

  • 2006年5月8日,Vim 7.0:拼写检查,自动补齐,撤消分支,标签。

  • 2016年9月12日,Vim 8.0:作业,异步I / O,本机包。

有关各个版本的详细信息,请查看:help vim8。如果想了解未来的计划,以及已知的bug,请查看:help todo.txt。

受到来自竞争对手NeoVim的压力,Vim 8.0加入了异步作业的支持,NeoVim的开发人员希望在编辑器中直接运行Web脚本的调试器和REPL。

Vim超级便携。在漫长的发展过程中,为了支持多种平台,vim本身不得不保持便携。它可以在各种平台上运行,包括OS / 390、Amiga、BeOS和BeBox、Macintosh classic、Atari MiNT、MS-DOS、OS / 2、QNX、RISC-OS、BSD、Linux、OS X、VMS和MS-Windows等。无论哪种计算机都可以使用Vim。

在vi发展历程的最后一个转折点上,最原始的ex / vi源代码最终于2002年在BSD免费软件许可下发布了。请点击这里获取(http://ex-vi.sourceforge.net/)。

下面干货来了。在深入Vim的使用技巧之前,先让我们了解一下Vim的组织以及读取配置文件的方式。


配置层次结构


我曾经错误地认为,Vim仅从〜/ .vimrc文件中读取其所有设置和脚本。阅读各种“dotfiles”的代码库更坚定了我的这一看法。通常人们觉得只通过一个.vimrc文件来控制编辑器的各个方面是一种危险的做法。这些庞大的配置文件有时被称为“vim发行版”。

实际上,Vim的结构非常整洁,.vimrc只是多个配置文件中的其中一个而已。其实,你可以让Vim告诉你究竟加载了哪些脚本。试试看:任意编辑计算机上的某个源代码文件。加载后,运行如下命令:

:scriptnames 

花点时间读完整个清单。猜猜看这些脚本可能会做些什么,并记下它们所在的目录。

清单比你预期的要长吗?如果你安装了大量插件的话,那么编辑器需要做大量工作。你可以通过以下命令检查是什么导致编辑器的速度变慢,然后再看看它创建的start.log:

vim --startuptime start.log name-of-your-file

为了比较起见,下面我们看看如果没有这些配置,Vim的启动速度有多快:

vim --clean --startuptime clean.log name-of-your-file

为了确定启动时或加载缓冲区时会运行哪些脚本,Vim会遍历“runtimepath”。该设置是一组以逗号分隔的目录列表,各个目录的结构都是一致的。Vim会检查每个目录的结构,找到需要运行的脚本,并按照目录在列表中的顺序一一处理。

运行以下命令就可以检查系统上的runtimepath:

:set runtimepath

在我的系统上,runtimepath默认包含以下目录。并非所有这些都必须出现在文件系统中,但如果存在就会被使用。

  • ~/.vim

    主目录,保存个人偏好的文件。

  • /usr/local/share/vim/vimfiles

    系统范围的Vim目录,保存由系统管理员决定的文件。

  • /usr/local/share/vim/vim81

    即$VIMRUNTIME,保存与Vim一起分发的文件。

  • /usr/local/share/vim/vimfiles/after

    系统范围Vim目录中的“after”目录。系统管理员可以利用该目录来覆盖默认设置,或添加新的设置。

  • ~/.vim/after

    主目录中的“after”目录。可以利用该目录用个人偏好覆盖默认设置或系统设置,或添加新的设置。

这些目录会按照顺序处理,所以要说“after”目录有什么特别的话,那就是它位于列表末尾。实际上“after”并没有什么特别之处。

在处理每个目录时,Vim都会查找具有特定名称的子文件夹。如果想了解更多这方面的信息,请参阅:help runtimepath。下面我们只挑部分进行说明。

  • plugin/

    编辑任何类型的文件都会自动加载的Vim脚本文件,称为“全局插件”。

  • autoload/

    (不要与“插件”相混淆。)自动加载中的脚本包含仅在其他脚本请求时加载的函数。

  • ftdetect/

    用于检测文件类型的脚本。可以根据文件扩展名、位置或内部文件内容决定文件类型。

  • ftplugin/

    编辑已知类型的文件时执行的脚本。

  • compiler/

    定义如何运行各种编译器或格式化工具,以及如何解析其输出。可以在多个ftplugins之间共享。且不会自动执行,必须通过 :compiler 调用。

  • pack/

    Vim 8原生软件包的目录,它采用了“Pathogen”格式的包管理。原生的包管理系统不需要任何第三方代码。

最后,通用的编辑器设置都会放到~/.vimrc中。你可以通过它来设置用于覆盖特定文件类型的默认值。有关.vimrc设置的全面讲解,请运行 :options。


第三方插件


在Vim中,插件只是脚本,必须放在runtimepath中的正确位置才能执行。从概念上讲,插件的安装非常简单:只需下载文件。问题在于,很难删除或更新某些插件,因为它们的子目录加入到了runtimepath中,很难判断哪个插件负责哪些文件。

为了满足这种需求,网上出现了很多插件管理器。最早在2003年就出现了Vim.org插件仓库。然而,直到2008年左右,插件管理器的概念才真正流行起来。

这些工具在Vim的runtimepath中添加了单独的查检目录,并会为插件文档编译帮助标签。大多数插件管理器还可以从网上安装和更新插件代码,有的还支持并行更新,或者显示彩色的进度条。

以下是按时间顺序整理的插件管理器。我按照每个插件最早和最新版本进行了排序,如果找不到官方的发行版本,则根据最早和最后的提交日期排序。

  1. 2006年3月- 2014年7月:Vimball(分发格式和关联的Vim命令)

  2. 2008年10月- 2015年12月:Pathogen(由于原生vim包被弃用)

  3. 2009年8月- 2009年12月:Vimana

  4. 2009年12月- 2014年12月:VAM

  5. 2010年8月 - 2010年12月:Jolt

  6. 2010年10月 - 2012年12月:tplugin

  7. 2010年10月 - 2014年2月:Vundle(在NeoBundle破解代码后停止使用)

  8. 2012年3月 - 2018年3月:vim-flavor

  9. 2012年4月 - 2016年3月:NeoBundle(被弃用,建议使用dein)

  10. 2013年1月 - 2017年8月:infect

  11. 2013年2月 - 2016年8月:vimogen

  12. 2013年10月 - 2015年1月:vim-unbundle

  13. 2013年12月 - 2015年7月:Vizardry

  14. 2014年2月 - 2018年10月:vim-plug

  15. 2015年1月 - 2015年10月:enabler

  16. 2015年8月 - 2016年4月:Vizardry 2

  17. 2016年1月 - 2018年6月:dein.vim

  18. 2016年9月 - 至今:原生Vim 8

  19. 2017年2月 - 2018年9月:minpac

  20. 2018年3月 - 2018年3月:autopac

  21. 2017年2月 - 2018年6月:pack

  22. 2017年3月 - 2017年9月:vim-pck

  23. 2017年9月 - 2017年9月:vim8-pack

  24. 2017年9月 - 2019年5月:volt

  25. 2018年9月 - 2019年2月:vim-packager

  26. 2019年2月 - 2019年2月:plugpac.vim

首先要注意,这些工具五花八门,其次通常每个工具在活跃大约四年后就会过时。

最稳定的管理插件的方法是使用Vim 8的内置功能,该功能不需要第三方代码。下面让我们具体来看看这种方法。

首先在运行时目录的pack目录中创建两个目录opt和start。

mkdir -p ~/.vim/pack/foobar/{opt,start}

注意占位符 foobar。这个名称完全取决于你。我们用它对包进行分类。大多数人会把所有的插件都扔进一个无意义的类别中,这样完全没问题。你可以选择自己喜欢的名称,在本文中我选择使用 foobar。理论上,你也可以创建多个类别,比如~/.vim/pack/navigation, ~/.vim/pack/linting等。请注意,Vim不会检测类别之间的重复,如果存在重复,则会加载两次。

“start”中的包会自动加载。而对于“opt”中的包,只有通过:packadd命令特别请求,Vim才会加载。opt中适合保存不常用的软件包,以及为保持Vim的快速启动不必要运行的脚本。请注意,:packadd没有相反的命令卸载包。

在下述示例子中,我们将添加“ctrlp”模糊查找插件到opt目录。下载最新版本的命令如下:

curl -L https://github.com/kien/ctrlp.vim/archive/1.79.tar.gz \
    | tar zx -C ~/
.vim/pack/foobar/opt

该命令创建了 ~/.vim/pack/foobar/opt/ctrlp.vim-1.79 文件夹,现在这个包可以使用了。我们再次回到vim中,为这个新包创建一个帮助标签的索引:

:helptags ~/.vim/pack/foobar/opt/ctrlp.vim-1.79/doc

该命令会在包的doc目录中创建了一个名叫”tags“的文件,这样Vim的内部帮助系统就可以使用它的内容了。(或者你也可以在包加载之后运行一次:helptags ALL,该命令会处理runtimepath下的所有文档。)

在需要使用包时,只需加载它(Tab自动补齐也可以用于插件名,所以不需要输入全名):

:packadd ctrlp.vim-1.79

packadd会把包的根目录放到runtimepath中,然后运行它的plugin和ftdetect脚本。在加载ctrlp之后,就可以按Ctrl-P来弹出模糊文件查找了。

有些人喜欢将~/.vim目录放到版本管理中,使用git submodules来管理每个包。而我喜欢简单地将包从tarball中解压,然后用自己的代码库来管理。如果你使用成熟的包,那么更新不会太频繁,加上脚本本身也很小,不会把git历史弄得太乱。


备份和undo


根据不同的用户设置,Vim可以防止四种类型的丢失:

  1. 编辑过程中(两次保存之间)崩溃。Vim会定期将未保存的修改写入交换文件来防止这种情况。

  2. 使用两个Vim进程编辑同一个文件,两个进程互相覆盖。交换文件也可以防止这种情况。

  3. 保存过程中崩溃,即在目标文件已被截断,新的内容尚未完全写入时崩溃。Vim可以通过“writebackup”来防止这种情况。为了实现该功能,Vim会首先将内容写入新的文件,写入成功后与原始文件交换。但这个功能取决于“backupcopy”设置。

  4. 已保存新文件,但想要找回原文件。Vim可以通过在写入改变后保留原始文件的备份来防止这种情况。

在介绍具体的设置之前,先来放松一下吧!下面是GitHub上人们对于vimrc的一些评论:

  • “不要创建交换文件。用版本控制管理就好。”

  • “素人才用备份。高手都用版本控制。”

  • “用版本控制就好!”

  • “版本控制都满天飞了,就不要再用交换文件和备份了。”

  • “不要写备份文件,版本控制就是很好的备份。”

  • “我其实从来没用过VIM的备份文件……一直都在用版本控制。”

  • “反正大部分东西都保存在版本控制里。”

  • “禁用备份文件,因为反正你也得用版本控制。”

  • “版本控制已来到,git拯救全世界。”

  • “禁用交换文件和备份(永远使用版本控制!永远!)”

  • “关掉备份,我所有东西都用版本控制。”

上面的评论反映出,大家只了解上述第四种情况(偶尔也会提及第三种情况),这些人倾向于把交换文件也禁用,这会让Vim无法防止第一种和第二种情况。

为了保证编辑更安全,我建议使用下述配置:

" Protect changes between writes. Default values of
" updatecount (200 keystrokes) and updatetime
" (4 seconds) are fine
set swapfile
set directory^=~/.vim/swap//

" protect against crash-during-write
set writebackup
"
 but do not persist backup after successful write
set nobackup
" use rename-and-write-new method whenever safe
set backupcopy=auto
"
 patch required to honor double slash at end
if has("patch-8.1.0251")
    " consolidate the writebackups -- not a big
    "
 deal either way, since they usually get deleted
    set backupdir^=~/.vim/backup//
end

" persist the undo tree for each file
set undofile
set undodir^=~/.vim/undo//

这些设置为写入过程启用了备份,但在成功写入后不会保留备份,因为我们有版本控制。注意你需要mkdir ~/.vim/{swap,undodir,backup},否则Vim会使用设置列表中的下一个可用的文件夹。你还应该chmod这些文件夹来保证隐私,因为交换文件和undo历史可能包含敏感信息。

关于配置中的路径,需要提及的一点是,它们末尾使用了双斜线。这样可以无歧义地表示不同目录下同名文件的交换文件和备份文件。例如,/foo/bar文件的交换文件会保存在~/.vim/swap/%foo%bar.swp(斜线z转义成百分号)。Vim有一个bug,对于backupdir不会正确处理双斜线写法,该bug直到最近才修复,而上述配置可以防止这个bug。

我们还要求Vim持久保存每个文件的undo文件,这样在退出Vim并重新编辑文件时依然可以使用undo。虽然有了交换文件,这样做有点多余,但实际上undo文件是补充性质的,因为它仅在原文件被写入时才写入。(如果undo文件写入太频繁,那么可能在崩溃后无法匹配磁盘上文件的状态,所以Vim不这样做。)

说起undo就不得不提起Vim会维持编辑历史的整个树形结构。这意味着你可以做一个修改,undo之后,然后做另一个修改,这时所有三个状态都可以被恢复。使用:undolist命令可以看到修改的时间和大小,但从该命令的结果很难想象整个树形结构。你可以遍历列表中的特定修改,也可以用:earlier和:later命令加上一个时间参数(如5m)或保存次数参数(如3f)在时间轴上移动。但是,遍历undo树最好使用插件——如undotree。

启用这些灾难恢复设置可以让你安心地使用Vim。我曾经在编辑过程中多次保存,或者每次离开电脑时也会保存,但现在我会几个小时都不保存,因为我知道交换文件在老老实实地干活。

最后几点:要时刻关注这些灾难恢复文件,时间长了它们可能会在.vim文件夹下越积越多,占用大量空间。另外,当磁盘剩余空间很少,却需要保存大文件时,也许有必要设置nowritebackup,否则Vim必须临时保存整个文件的副本。默认设置下“backupskip”设置能够禁用系统临时目录下的任何文件的备份。

Vim的“patchmode”与备份有关。你可以在没有被版本控制管理的目录下使用该设置。例如,如果你想下载源代码tar包,做一些修改然后通过邮件列表提交补丁,这一过程中不使用git。只需运行:set patchmod=.orig,那么任何Vim写入的文件“foo”就会备份成“foo.orig”。然后可以通过命令行比较.orig文件和新文件来创建补丁。


包含和路径


绝大多数编程需要都允许你在一个文件中包含另一个模块或文件。Vim通过path、include、suffixesadd和includeexpr配置项来了解如何跟踪包含文件中的程序标识符。标识符搜索(参见:help include-search)是另一种使用ctags维持系统头文件的标签文件的方式。

C程序的默认设置工作得很好。其他语言也同样支持,但需要一些设置。这些设置超出了本文的范围,可以参考:help include。

如果一切配置正确,那么你可以在标识符上按 [i 来显示标识符定义,或者在宏常量上按 [d 显示宏定义。还有,在文件名上按 gf 可以搜索路径并跳转到相应的文件。由于路径也会影响 :find 命令,一些人倾向于在路径中添加“**/*”或常用的目录,把 :find 命令当作简装版的模糊查找使用。但这样做会减慢标识符搜索的速度,因为它需要搜索与标识符搜索无关的目录。

不污染路径而实现相同查找功能的方式之一就是建立一个映射。这样只需按<Leader><space>(通常这两个键就是反斜杠然后空格)然后输入文件名,再使用Tab或Ctrl-D自动完成来查找文件。

fuzzy-find lite
nmap <Leader><space:e ./**/

重申一下:路径参数是为头文件准备的。如果你想看更多证据,还可以用:checkpath命令显示哪些路径有效。加载一个C文件然后运行:checkpath,它就会显示那些当前文件包含,却找不到的文件名。带感叹号的 :checkpath! 可以显示当前文件包含的整个头文件层次结构。

默认情况下,路径的值为“.,/usr/include,,”,意思是当前目录、/usr/include,然后是当前活动缓冲区的所有兄弟文件。目录指定符和glob非常强大,详情可以查看:help file-searching。

我还在C ftplugin中(后文会多次提到它),让路径搜索包含了当前项目的包含文件,如./src/include或./include。

setlocal path=.,,*/include/**3,./*/include/**3
setlocal path+=/usr/include

带数字的 ** (如**3)指定子目录搜索的深度。最好在这里指定深度,以免标识符搜索锁死。

如果 :checkpath 指示出项目中找不到的文件,那么也可以考虑将下面这些模式添加到路径中。当然,这完全取决于你的系统。

  • 更多的系统包含文件:/usr/include/**4,/usr/local/include/**3

  • Homebrew库的头文件:/usr/local/Cellar/**2/include/**2

  • Macports库的头文件:/opt/local/include/**

  • OpenBSD库的头文件:/usr/local/lib/\*/include,/usr/X11R6/include/\*\*3

另请参考::he [,:he gf,:he :find。


编辑-编译循环


:make 命令会执行用户选择的程序来构建项目,然后将输出收集到quickfix缓冲区中。quickfix记录中的每一项都记录了文件名、行号、列号、类型(警告或错误)和消息。一种常见的使用方括号命令的映射方式如下,可以在quickfix项目中快速移动:

" quickfix shortcuts
nmap ]q :cnext<cr>
nmap ]Q :clast<cr>
nmap [q :cprev<cr>
nmap [Q :cfirst<cr>

如果在更新程序并重新编译后,你想知道上次的消息,可以使用 :colder 命令(使用 :cnewer 返回)。如果需要查看有关当前错误的更多信息,可以使用 :cc ,然后用 :copen 命令查看完整的quickfix缓冲区。还可以使用 :cile、:caddfile 或 :cexpr 命令,无需运行:make而自行填充quickfix缓冲区。

Vim能够利用指定的errorformat字符串解析编译的输出。errorformat是个类似scanf的转义序列。例如,Vim的gcc设置($VIMRUNTIME/compiler/gcc.vim)中自带了errorformat设置,但却没有包含clang编译器的设置。于是我创建了下面的定义:

" formatting variations documented at
"
 https://clang.llvm.org/docs/UsersManual.html#formatting-of-diagnostics
"
"
 It should be possible to make this work for the combination of
" -fno-show-column and -fcaret-diagnostics as well with multiline
"
 and %p, but I was too lazy to figure it out.
"
"
 The %D and %X patterns are not clang per se. They capture the
" directory change messages from (GNU) 'make -w'. I needed this
"
 for building a project which used recursive Makefiles.

CompilerSet errorformat=
    \%f:%l%c:{%*[^}]}{%*[^}]}:\ %trror:\ %m,
    \%f:%l%c:{%*[^}]}{%*[^}]}:\ %tarning:\ %m,
    \%f:%l:%c:\ %trror:\ %m,
    \%f:%l:%c:\ %tarning:\ %m,
    \%f(%l,%c)\ :\ %trror:\ %m,
    \%f(%l,%c)\ :\ %tarning:\ %m,
    \%f\ +%l%c:\ %trror:\ %m,
    \%f\ +%l%c:\ %tarning:\ %m,
    \%f:%l:\ %trror:\ %m,
    \%f:%l:\ %tarning:\ %m,
    \%D%*\\a[%*\\d]:\ Entering\ directory\ %*[`']%f',
    \%D%*\\a:\ Entering\ directory\ %*[`
']%f',
    \%X%*\\a[%*\\d]:\ Leaving\ directory\ %*[`']%f',
    \%X%*\\a:\ Leaving\ directory\ %*[`
']%f',
    \%DMaking\ %*\\a\ in\ %f

CompilerSet makeprg=make

要激活该编译器设置,只需运行 :compiler clang。通常该命令在ftplugin文件中执行。

另一个例子是在文本文件上运行GNU Diction来识别句子中用错的词汇和短语。可以创建一个“编译器”,名为diction.vim:

CompilerSet errorformat=%f:%l:\ %m
CompilerSet makeprg=diction\ -s\ %

运行 :compiler diction 之后,可以使用 :make 命令来运行,并填充quickfix。最后,我在.vimrc中添加了一个映射来运行make:

" real make
map <silent> <F5> :make<cr><cr><cr>
" GNUism, for building recursively
map <silent> <s-F5> :make -w<cr><cr><cr>


差异文件和补丁


Vim自带的比较工具非常强大,但可能有点难用,特别是三方合并视图。但实际上花点时间学习你就会发现其实挺好用的。要点就是,每个窗口都可以处于或不处于“diff mode”。所有处于diffmode的窗口(用:difft[his]设置)会与所有其他已经处于diffmode的窗口进行比较。

我们从一个简单的例子开始。首先创建两个文件:

echo "hello, world" > h1
echo "goodbye, world" > h2

vim h1 h2

在vim中运行 :all 命令,将上述参数指定的文件分别放入各自的窗口中。在上方的h1的窗口中运行 :difft。你会看到出现了一个分割线,但没有检测到任何差异。用Ctrl-W Ctrl-W移动到下方窗口,然后运行 :difft。这时就会检测出hello和goodbye之间的差异。在下方窗口中执行 :diffg[et] 可以从上方窗口中拉取“hello”,或者使用 :diffp[ut] 将“goodbye”发送到上方窗口。如果有多个差异块,那么按 ]c 或 [c 可以在不同的差异块中移动。

快捷方式之一就是运行 vim -d h1 h2 (或者运行其别名 vimdiff h1 h2),该命令会对所有窗口执行 :difft。此外,还可以先用vim h1仅加载h1,然后执行 :diffsplit h2。记住,所有这些命令实际上都是将文件加载到窗口中并设置diffmode而已。

了解这些基本知识后,我们来学习怎样把Vim作为git的三方合并工具使用。首先配置git:

git config merge.tool vimdiff
git config merge.conflictstyle diff3
git config mergetool.prompt false

现在,当遇到合并冲突时,只需运行git mergetool。该命令会启动Vim并打开四个窗口。这部分看上去很吓人,我经常会举棋不定。

+-----------+------------+------------+
|           |            |            |
|           |            |            |
|   LOCAL   |    BASE    |   REMOTE   |
+-----------+------------+------------+
|                                     |
|                                     |
|             (edit me)               |
+-------------------------------------+

关键在于所有编辑都应该在下方窗口中进行。上方的三个窗口仅用于提供文件差异(local和remote)的上下文,以及每一方在修改之前的样子(base)。

使用 ]c 命令在下方窗口中移动,针对每个差异块,可以选择local、base或remote之一来替换,或者可以自己修改,合并多方的内容。

为了能够更容易地从上方窗口拉取修改,我在vimrc里设置了一些映射:

" shortcuts for 3-way merge
map <Leader>1 :diffget LOCAL<CR>
map <Leader>2 :diffget BASE<CR>
map <Leader>3 :diffget REMOTE<CR>

我们已经介绍过了 :diffget,上述绑定会为其传递一个参数,即用来识别拉取源的缓冲区名。

合并结束后,执行 :wqa 保存所有窗口并退出。如果你想放弃合并,可以运行 :cq 放弃所有修改,给shell返回一个错误代码。该错误代码会告诉git应当忽略这些修改。

diffget还可以接受范围。如果想从某个上方窗口拉取所有差异块,而不想逐个拉取,可以执行 :1,$+1diffget {LOCAL,BASE,REMOTE} 。“+1”是必要的,因为缓冲区的最后一行的“下方”可能存在被删除的行。

毕竟,三方合并其实很简单。至少,不需要用Fugitive之类的插件在合并冲突时显示差异。

最后,8.1.0360版本中包含了xdiff库,可以直接创建diff文件。这比使用外部程序更有效率,而且可以采用多种diff算法。“patience”算法通常可以生成比默认设置更容易阅读的输出。在.vimrc中这样设置:

if has("patch-8.1.0360")
    set diffopt+
=internal,algorithm:patience
endif


缓冲区I/O 


看看这是不是很熟悉?你编辑了一个缓冲区,想把它保存成新文件,所以执行了:w newname。再次进行一些编辑后,执行 :w ,但却保存到了原始文件上。在这种情况下,你真正需要的是 :saveas newname,即写入新文件,并将缓冲区的文件名改为新文件,方便以后的写入。此外,:file newname命令可以改变缓冲区文件名,而不会执行实际的写入。

学习更多有关读写命令的知识也很有用。因为r和w都是ex的命令,所以它们都可以接受范围。下面是一些你不太熟知的使用方法:

  • :w >> foo

    将整个缓冲区追加到文件中

  • :.w >> foo

    将当前行追加到文件中

  • :$r foo

    读取foo并插入到缓冲区末尾

  • :0r foo

    读取foo并插入到开头,已有行向下移动

  • :.,$w foo

    将当前行以及之后的所有行写入文件

  • :r !ls

    读取ls输出到当前光标位置

  • :w !wc

    将缓冲区发送到wc命令然后显示结果

  • :.!tr 'A-Za-z' 'N-ZA-Mn-za-m'

    为当前行执行ROT-13

  • :w | so %

    连锁命令:写入并执行缓冲区

  • :e!

    放弃为保存到修改,重新加载缓冲区

  • :hide edit foo

    编辑foo,如果当前缓冲区被修改过,则隐藏

冷知识:上面的例子中使用一整行来调用 tr 以实现ROT-13加密,但实际上Vim内置了该功能,即 g? 命令。可以将其应用到移动操作,如 g?$。


filetypes


filetypes设置可以根据缓冲区中检测到到文件类型来改变设置。不过它们并不一定非要自动检测,我们可以手动启用它们,实现一些有趣的效果。一个例子就是十六进制编辑。任何文件都可以作为十六进制值查看。GitHub用户the9ball写了一个非常聪明的ftplugin脚本,可以将缓冲区传递给xxd或传回,实现十六进制编辑。

为了方便使用,Vim 5版本捆绑了xxd工具。Vim的todo.txt提到,他们想让二进制文件编辑功能更加顺畅,但xxd已经实现了不少功能。 

将下面的代码放到 ~/.vim/ftplugin/xxd.vim 中。保存到ftplugin中的意思是,每当filetype(即“ft”)变成xxd时,Vim就会执行该脚本。我在脚本中添加了一些简单的注释:

" without the xxd command this is all pointless
if !executable('xxd')
    finish
endif

" don't insert a newline in the final line if it
" doesn't already exist, and don't insert linebreaks
setlocal binary noendofline
silent %!xxd -g 1
%s/\r$//e

"
 put the autocmds into a group for easy removal later
augroup ftplugin-xxd
    " erase any existing autocmds on buffer
    autocmd! * <buffer>

    "
 before writing, translate back to binary
    autocmd BufWritePre <buffer> let b:xxd_cursor = getpos('.')
    autocmd BufWritePre <buffer> silent %!xxd -r

    " after writing, restore hex view and mark unmodified
    autocmd BufWritePost <buffer> silent %!xxd -g 1
    autocmd BufWritePost <buffer> %s/\r$//e
    autocmd BufWritePost <buffer> setlocal nomodified
    autocmd BufWritePost <buffer> call setpos('.', b:xxd_cursor) | unlet b:xxd_cursor

    "
 update text column after changing hex values
    autocmd TextChanged,InsertLeave <buffer> let b:xxd_cursor = getpos('.')
    autocmd TextChanged,InsertLeave <buffer> silent %!xxd -r
    autocmd TextChanged,InsertLeave <buffer> silent %!xxd -g 1
    autocmd TextChanged,InsertLeave <buffer> call setpos('.', b:xxd_cursor) | unlet b:xxd_cursor
augroup END

" when filetype is set to no longer be "xxd," put the binary
"
 and endofline settings back to what they were before, remove
" the autocmds, and replace buffer with its binary value
let b:undo_ftplugin = 'setl bin< eol< | execute "
au! ftplugin-xxd * <buffer>" | execute "silent %!xxd -r"'

打开一个文件,然后执行 :set ft。记下文件类型。然后执行 :set ft=xxd。Vim就会变成一个十六进制编辑器。要恢复原来的视图,只需 :set fo=foo,其中foo是原始的文件类型。注意十六进制视图甚至还有语法高亮,因为Vim默认自带了 $VIMRUNTIME/syntax/xxd.vim 。

注意这里的“b:undo_ftplugin”非常巧妙,它可以在用户或ftdetect机制将文件类型切换成其他filetype时,让filetypes执行一些清理工作。(上面的例子还可以改进一下,因为如果你 :set ft=xxd 然后直接改回去,那么缓冲区会被标记为已修改,即使你没有进行任何修改。)

ftplugins还可以进一步定义已知的filetype。例如,Vim已经在 $VIMRUNTIME/ftplugin/c.vim 中为C语言包含了非常好的默认设置。我在 ~/.vim/after/ftplugin/c.vim 中添加了额外的选项:

" the smartest indent engine for C
setlocal cindent
"
 my preferred "Allman" style indentation
setlocal cino="Ls,:0,l1,t0,(s,U1,W4"

" for quickfix errorformat
compiler clang
"
 shows long build messages better
setlocal ch=2

" auto-create folds per grammar
setlocal foldmethod=syntax
setlocal foldlevel=10

"
 local project headers
setlocal path=.,,*/include/**3,./*/include/**3
" basic system headers
setlocal path+=/usr/include

setlocal tags=./tags,tags;~
"
                      ^ in working dir, or parents
"                ^ sibling of open file

"
 the default is menu,preview but the preview window is annoying
setlocal completeopt=menu

iabbrev #i #include
iabbrev #d #define
iabbrev main() int main(int argc, char **argv)

" add #include guard
iabbrev #g _<c-r>=expand("
%:t:r")<cr><esc>VgUV:s/[^A-Z]/_/g<cr>A_H<esc>yypki#ifndef <esc>j0i#define <esc>o<cr><cr>#endif<esc>2ki

注意上述脚本使用了“setlocal”而不是“set”。它仅对当前缓冲区生效,而不是对整个Vim进程生效。

该脚本还添加了一些缩写。例如,我可以输入 #g 并按回撤,就能自动使用当前文件名添加包含检测:

#ifndef _FILENAME_H
#define _FILENAME_H

/* <-- cursor here */

#endif

你还可以使用点(“.”)来混合多种filetypes。下面是应用的例子。不同的项目有不同的编码规范,所以你可以将默认的C设置与特定项目的设置结合起来。OpenBSD的源代码遵循style(9)格式(https://man.openbsd.org/style.9),所以我们来做一个特殊的openbsd filetype。可以在相关文件上使用 :set ft=c.openbsd 将两个filetype合并。

要检测openbsd filetype,可以查看缓冲区的内容,而不仅仅是通过文件扩展名或文件在磁盘上的位置。C文件中包含OpenBSD源代码的标志就是第一行出现 /* $OpenBSD: 。

创建 ~/.vim/after/ftdetect/openbsd.vim 进行检测:

augroup filetypedetect
        au BufRead,BufNewFile *.[ch]
                \  if getline(1) =~ 'OpenBSD;'
                \|   setl ft=c.openbsd
                \|
 endif
augroup END

OpenBSD的Vim移植已经包含了该filetype的特殊语法:/usr/local/share/vim/vimfiles/syntax/openbsd.vim。回忆一下,/usr/local/share/vim/vimfiles目录位于runtimepath中,用于保存系统管理员提供的文件。该openbsd.vim脚本包含下面的函数:

function! OpenBSD_Style()
    setlocal cindent
    setlocal cinoptions=(4200,u4200,+0.5s,*500,:0,t0,U4200
    setlocal indentexpr=IgnoreParenIndent()
    setlocal indentkeys=0{,0},0),:,0#,!^F,o,O,e
    setlocal noexpandtab
    setlocal shiftwidth=8
    setlocal tabstop=8
    setlocal textwidth=80
endfun

我们只需在适当时候调用该函数。创建 ~/.vim/after/ftplugin/openbsd.vim:

call OpenBSD_Style()

现在打开任何顶部具有标志性注释的C文件或头文件,就会被识别为c.openbsd类型,从而采用style(9)手册页中规定的缩进选项。


别忘了鼠标


在此友好地提醒你,尽管我们都喜欢命令行,但实际上Vim也支持鼠标,而且有些任务比键盘更方便。由于xterm能够将鼠标事件转换为stdin转义代码,所以我们甚至可以通过SSH都能支持鼠标事件。

如果想启用鼠标支持,则需要设置 mouse=n。许多人喜欢设置 mouse=a,因为这样就可以在所有模式下工作,但我更喜欢只在普通模式下启用鼠标支持。这样,在我用键盘加点击的方式在浏览器中打开链接时,就不会错误地创建可视选择区域。

以下是鼠标可以执行的操作:

  • 打开或关闭折叠(当foldcolumn> 0时)。

  • 选择标签(比 gt gt gt gt ...要好用得多)

  • 单击完成动作,例如 d<点击>。类似于easymotion插件,但不需要任何插件。

  • 双击即可跳转到帮助主题。

  • 拖动底部的状态行以更改cmdheight。

  • 拖动窗口边缘以调整大小。

  • 鼠标滚轮。


其他编辑功能


这部分涉及的内容很杂,但我仅在此介绍一些我学到的技巧。第一个让我感到震惊的是::set virtualedit=all。它允许你将光标移动到窗口中的任何位置。如果你输入字符或插入可视块,Vim会在插入的字符的左侧添加所需的空格以保证它们的位置。虚拟编辑模式可以简化表格数据的编辑。你可以通过 :set virtualedit= 来关闭这个选项。

接下来是一些移动命令。在跳转到下一段时,我习惯于使用 } ,每次跳转一个段落。然而, ] 字符可以完成更精准的跳转:跳转到下一个函数 ]]、作用域 ]}、圆括号 ‘])’、注释 ]/、差异块 ]c。前面提到的 quickfix 映射 ]q 也是这种操作方式之一。

对于大段的跳转,我曾经尝试过 1000j 等操作,但实际上只需在普通模式下键入百分比,Vim就会跳转到相应的位置,比如50%。说到滚动百分比,你随时可以使用CTRL-G查看它。所以现在我采用了 :set noruler 的设置,只在需要了解百分比的时候查看,这样画面就不会过于杂乱了。这似乎与色彩斑斓的powerlines的流行趋势有点背道而驰。

如果想在标签、文件或文件中跳转,那么有些命令可以帮助你。比如::ls、:tags、:jumps 和 :marks。在标签之间跳转实际上会创建一个栈,你可以按CTRL-T跳到前一个。以前我经常按CTRL-O退出跳转,但是它不如弹出标签栈那般直接。

在使用ctags编制索引的项目目录中,你可以使用 -t 选项在打开编辑器时直接跳到标签,比如:vim -t main。如果想更灵活地查找标签文件,那么可以设置 tags 配置变量。请注意如下示例中的分号,有了它Vim就可以从当前目录向上搜索到主目录。如此一来,你就可以在项目文件夹外部使用更通用的系统标记文件。

set tags=./tags,**5/tags,tags;~
"                          ^ in working dir, or parents
"
                   ^ in any subfolder of working dir
"           ^ sibling of open file

此外,还有一些缓冲区技巧。切换缓冲区的命令 :bu 可以接受缓冲区名称的片段作为参数,而不仅仅是数字。有时很难记住这些数字,相比之下源文件的名称更加方便记忆。你也可以使用标记来浏览缓冲区。如果使用大写字母作为标记的名称,则可以跨缓冲区跳转到该标记。你还可以在标题中设置标记H,在源文件中设置C,在Makefile中设置M,这样就可以在缓冲区之间来回跳转了。

你有没有遇到过这种情况:复制一个单词,然后在其他地方删掉一个单词,当尝试粘贴第一个单词时,却发现原来复制的单词已被覆盖。是不是很气恼?Vim寄存器不善于处理这种情况。你可以用 :reg 检查其内容。当你复制文本时,先前的复制就会被轮换到寄存器"0 - "9。因此,"0p 会粘贴倒数第二个复制/删除。特殊寄存器 "+ 和 "* 可以从系统剪贴板中复制/粘贴,也可以复制/粘贴到系统剪贴板。通常,这两者的含义相同,除了在一些X11设置中会区分首选和备选。

另一个非常方便的隐藏功能是命令行窗口。它是一个缓冲区,其中包含了你以前运行的命令和搜索。你可以通过 q: 或 q/ 显示该窗口。在进入该缓冲区后,你可以随意移动到任何一行,然后按Enter键运行该行的命令。然而,你也可以在按Enter键之前对行进行编辑。你的更改不会影响该行(仅会将新的命令将添加到列表的底部)。

vim的使用技巧繁多,文本无法详尽阐述。如果你想了解更多信息,请参阅帮助文档:views-sessions、viminfo、TOhtml、ins-completion、cmdline-completion、multi-repeat、scroll-cursor、text-objects、grep、netrw-contents。

原文:https://begriffs.com/posts/2019-07-19-history-use-vim.html

本文为 CSDN 翻译,转载请注明来源出处。

【END】

 热 文 推 荐 

☞ 今日头条 7 年,好的算法还可以做什么?

为何敲代码,学好数学很重要?

网易云音乐工程师,亲自揭晓消息队列改造之路!| 技术头条

☞ 90 后程序员薪资大揭秘:有人刚毕业年薪 200 万,有人月薪不足 1 万

☞ 一文读懂Bakkt:加密货币的纽交所

☞ 学阿里中台,80%的人只学到了皮毛!揭秘阿里中台的12个架构思维和原则

☞ 真正适合小白的教程:Python有什么用?数据化运营怎么做?

☞ 效果惊人!中科院、百度研究院等联合提出UGAN,生成图片难以溯源

☞ 为什么雷军说“华为不懂研发”?

点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。

你点的每个“在看”,我都认真当成了喜欢

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存