介绍Vim的基本操作以及一些常用的高级技巧
截止到上一篇文章，关于vim的基础操作都已经讨论完了，这篇我主要就是闲扯，瞎聊。就想毕业论文都有一个致谢一样，这篇我们就作为整个系列的致谢吧学习vim到底能给我们带来什么学习vim到底能给我们带来什么呢？工作中很少有用会用vim来做主力编辑器，现在有各种现代化的编程工具，像JB 全家桶、visual studio 系列。它们从上手难度和集成化程度来说，都做的比较好，离开vim也能编程。而且vim本身也不能给你的简历带来什么亮点，没有公司招人的时候会要求熟练掌握vim、也没有人在简历上写自己熟练使用vim。面试时也没有面试官会问你vim相关的内容，反倒是你用学习vim的时间去学一门新的编程语言，像 go、rust 之类的能给你带来一份新的工作，能带来涨薪。从这个上面看，学习 vim 似乎变得有那么些鸡肋甚至有一种耽误时间的感觉。我最开始学习vim的初衷是，我需要在linux下进行一些环境的搭建，例如Apache、nginx、或者其他的服务。在linux的终端中，vim算是标配，几乎每个linux服务器都会安装vim。那个时候我只会按 i 进入插入模式、按方向键移动光标。它对我来说就是一个linux上比记事本还麻烦的一个文本编辑器。后来我看了 《程序员修炼之道》、《程序员的呐喊》这两本书，书中提到：我们程序员平时会花大量的时间与代码、文本编辑器打交道，如果每天抽出一些时间花在优化编辑器上将会大有好处。而且《程序员的呐喊》这本书十分推崇emacs。我遵照书中的意思学了一段时间的emacs ，发现emacs 中最高效的编辑方式是一个叫做 evil 的插件，它是一个 vim 的模拟器。而且后续我了解到很多编辑器都有相关的vim插件。这个时候我开始意识到vim并没有我之前想象的那么简单。随着emacs 的学习和使用进入瓶颈，我意识到要想用好 emacs 首先还是得学会如何使用 vim。这个时候我立马入了 vim 的坑。而且通过学习 vim，我越来越觉得 vim 给我带来的好处大于学习任何一种编辑器。首先，vim 最被推崇的是它的一套文本操作方式，例如为了减少快捷键的按键次数以及为了更好的抽象现实中编辑文本的场景，它使用了分模式的办法，不同模式下不同按键有不同的功能。在这它将文本进行分级，分为字符、单词、字串、句子、段落并且提出了文本对象的概念，极大方便了我们处理文本的速度。同时它也有 . 命令和 宏的操作，进一步简化了重复操作。再者它极大的继承了 unix 的哲学，专门的软件做专门的事并且将这个事做到极致。它很方便的和外部程序做集成，扩大了功能范围。最后就是它的高可定制性，使用者可以方便的根据自身的工作场景做定制，做出符合自己的编辑器。如今 vim 已经不单单指一个软件了，而是一整套完善的文本编辑技术，学好了这个，后续在任何编辑器上都可以用到，例如各种编辑器IDE都支持vim 快捷键或者vim 插件。可以做到无缝切换编辑环境。另外学习 vim 给我培养了一种解决问题并从中学习的思路：根据实际场景提出问题-->找到解决办法--> 寻找更好的办法-->学到新知识-->将新知识-->使用新的知识更好的解决之前的问题。我想我通过前面的一些文章已经传递了这一思想，例如根据所学知识不断的完善 在每行最后添加分号 这一操作。使用 . 或者 宏来操作重复内容等等。甚至还有小伙伴在评论区给出更好的解决方案，这些都是这一思路的体现。由于不断有新知识，而且知识可以很快的运用到工作中。学习vim的过程有很好的正反馈，以前需要不停用鼠标点或者需要自己手工完成的操作，现在只需要几秒钟或者几分钟就由vim自动完成的这一喜悦使我在学习vim的过程中一直乐在其中。我想这就是我学习vim和使用vim的意义和快乐所在吧写这一系列文章的心路历程最开始学习vim的时候我很困惑，读vim的用户手册显的干巴巴的，读的头昏脑涨，记得的不多，基本合书就忘。网上的教程很多都是直接罗列命令，跟用户手册差不多。或者直接写一堆配置告诉你vim可以配置的很好用。这些充斥着网络，但是又不是我这种初学者需要的。我希望的是有一个教程在实际使用中循序渐进的帮我掌握vim这些知识点，需要一个教程跟着它进行操作我就能理解并在实际中使用vim。我需要一个vim手册和实际使用的一个桥梁。很遗憾的是我没有找到我想要的教程。那么我就自己写吧，我相信肯定有vim的初学者跟我当初遇到的情况一样，我想把我心目中认为最适合我的教程写出来，没准它也适合其他人。在写这一系列文章的过程中，我深刻体会到，在如今互联网环境中，一旦写出什么东西，这个东西马上就不属于你。当然这个并不是再说有人抄袭之类的。我只是在感叹互联网中的内容传播速度，从我更新第一篇注水的内容开始，马上就有人关注并且给我评论说支持我。这无疑给我了很大的勇气，让我慢慢更新完这一系列文章。中间也有白天上班、晚上整理资料、写博客，白天在抽时间发表的时候，有时候也挺累，偶尔也想休息一下或者断更，中间有想法更新我觉得更有意思的内容。但是想想那些因为我的这些文章而关注我的人，不知道他们看到我在断更之后会是怎样的失落，这种好不容易找到适合自己的本想好好跟着学习，结果却被迫中断的这种心情，我很能体会。既然如此那就硬着头皮更新吧。这一系列的文章一旦开始立项、更新，那么它就不属于我自己，而属于各位希望通过这些文章学到点东西的小伙伴。我有义务将它们一一发布出来。中间也有不少小伙伴私行我，告诉我他们通过我的文章学到很多东西，也重拾了对 vim 的兴趣。这些鼓励的声音给了很大的帮助。特别是 知乎的用户 @ugvibib。最开始是他一直在给我评论分享自己的学习心得，也是他不停给我私信提醒我某些地方有错别字或者排版有错误。这些文章有他一部分的功劳，感谢 @ugvibib 负责给我校对。在写这些文章的过程中，我真的体会到“您的点赞关注评论是对我最大的支持”这句话并不完全是骗赞，骗流量的空话。我在更新这一系列文章的过程中也真心的希望得到反馈，得到关注，让我知道我写的这些破玩意还是有人看的，有人能从中获益，这些并不是我自己在这自说自话，自嗨，这些也并不是网络垃圾。有人关注、有人评论和点赞给我了继续更新下去的动力。后面该干什么关于vim的基础操作到此为止就全部更新完了，但这并不是vim的全部。后面该如何学习vim呢？通读vim用户手册，相信通过这些文章的学习各位小伙伴再重读vim手册也不会感到无所适从。我们可以从vim手册中找到比我介绍的更适合自己的操作方式。形成一套只适合自己的操作流程开始学习 vimscript 慢慢定制自己的vim并不是所有的场合都允许使用 vim 的。下面可以考虑将vim 的操作方式转移到其他编辑器上，例如 visual studio code、emacs 等等。最后感谢各位关注和鼓励我的小伙伴，下一个专栏我想继续写vim相关的。暂时定为写 vimscript 和vim配置相关的内容吧。
这篇文章我们将讨论 vim 自带的自动补全功能。当然，针对自动补全功能有许多好用的插件，但是了解vim自带的功能有助于我们更好的用来插件的补全功能。因为我见过有的配置文件将插件的功能配置的比原有的更难用，而且只用基本的功能不一定有原版的好用。所以这里也介绍一下原始版本用法，算是帮助各位在以后的配置中提供一个标杆。make 命令在了解自动补全之前，让我们先简单聊聊 :make 这个命令，它与上一篇文章中介绍的 :grep 命令类似，也是对 shell 命令的一个封装。它默认封装的是 make 命令。我们对 c/c++ 语言执行 :make 也就是在调用 shell 中的 make 命令。它会将编译产生的错误信息存储在 quickfix 列表中。我们上一节中介绍了如何操作 quickfix 列表。也介绍了如何对 :grep 命令进行改造。同样的 :make 也支持使用相同的方法进行改造。:make 命令中，使用 makeprg 来执行外部命令，使用 errorformat 来格式化输出到 quickfix 中。它们默认的值如下:makeprg="make" errorformag="errorformat=%*[^"]"%f"%*\D%l: %m,"%f"%*\D%l: %m,%-G%f:%l: (Each undeclared identifier is reported only once,%-G%f:%l: for each function it appears in.),%-GIn file included from %f:%l:%c:,%-GIn file included from %f:%l:%c\,,%-GIn file incl uded from %f:%l:%c,%-GIn file included from %f:%l,%-G%*[ ]from %f:%l:%c,%-G%*[ ]from %f:%l:,%-G%*[ ]from %f:%l\,,%-G%*[ ]from %f:%l,%f:%l:%c:%m,%f(%l):%m,%f:%l:%m,"%f"\, line %l%*\D%c%*[^ ] %m,%D%*\a[%*\d]: Entering directory %*[`']%f',%X%* \a[%*\d]: Leaving directory %*[`']%f',%D%*\a: Entering directory %*[`']%f',%X%*\a: Leaving directory %*[`']%f',%DMaking %*\a in %f,%f|%l| %m"可以调整它们的值来适配不同的外部命令。这里就不再详细展开了，相信阅读过上一篇文章的小伙伴对这个应该不陌生。本来 :make 命令是vim中十分有用的一个命令，应该单独写一篇文章的。但是它于 :grep 重复度太高了，所以我决定在介绍其他内容的时候一笔带过。想了解详细信息的可以参考vim的用户手册。自动补全自动补全可以在插入模式下触发，当我们触发补全功能的时候，vim会根据当前编辑会话中所有缓冲区的内容建立一张补全列表，然后根据当前光标左侧的字符进行检测，看在表中能否找到单词的一部分，能找到则会用这个未完成的单词对补全列表进行过滤，所以不是以它为开头的单词都被过滤掉，剩余的组成一个弹出式菜单供用户选择。效果如下：上述例子中，因为以 re 开头的原本只有 require 一项，为了展示补全效果这里我们新增一个以 re 开头的 return我们使用 <Ctrl +p> 和 <Ctrl + n> 来切换补全菜单中的上一条和下一条。除了这个，我们还有其他的用于操作补全菜单的快捷键。<Ctrl - n> : 使用来自补全列表中的下一项内容(next)<Ctrl - p> : 使用来自补全列表中的上一项内容(prev)<Down> : 与 <Ctrl -n> 相同<Up>: 与 <Ctrl - p> 相同<Ctrl -y> : 确认使用当前选中的匹配项<Ctrl - e> : 还原最初的输入项<Ctrl -h> : 从当前匹配项中删除一个字符<Ctrl - l> : 从当前匹配项中增加一个字符一般在输入字符的时候，如果有匹配项可以匹配vim会自动弹出，或者也可以手动使用 <Ctrl - n> 弹出匹配项菜单。在确定要使用的匹配后可以使用 <Ctrl-y> 来确认有时候虽然弹出了匹配项菜单，但是匹配项太多了，而你需要的单词又在列表的太后面，这个时候可以使用 <Ctrl - e> 来退出菜单，手动输入几个字符使匹配项更加精确。或者也可以输入 <Ctrl -p> 到达最开始的项，即我们目前的输入，然后再次输入字符来精简菜单项，接着使用 <Ctrl - n> 弹出菜单。使用这种方式来一步一步的逼近我们想要的结果自定义补全项来源默认情况下，vim 补全项主要来源于以下几个地方：缓冲区列表：vim补全项最基本的来源就是当前的缓冲区列表。它可以通过 <Ctrl - x><Ctrl - n> 来触发该项。包含文件，所有的编程语言都有包含文件的概念，例如 c/c++中的 #include , python 中的 import 。使用 <Ctrl-x><Ctrl-i> 可以触发这个选项，让vim从被包含文件中提取补全项。vim本身使用 c 语言编写的，它能够识别 c/c++ 语言中的关键字，我们可以指定 include 项来使 vim认识其他不同的关键字。一般常用的编程语言 vim 都能够识别，因此不需要修改 include 项。标签文件，我们使用 ctags 或者类似的插件的时候会生成一个标签文件，该文件会将扫描到代码中的关键字、函数、变量等的索引放入到一个文件中以供后续进行跳转。同时他们也会产生一系列的补全项到补全列表中。可以使用 <Ctrl+x><Ctrl+]> 来触发一般直接使用 <Ctrl + n> 触发的是当前缓冲区列表中的补全项，使用 <Ctrl+x> 作为前缀，可以触发其他类型的补全项。这么做有一个好处是尽量精简补全列表，减少了我们手动遍历的过程。但是有时候我们并不知道我想要的内容该从哪里来，有没有什么办法能做到，用 <Ctrl + n> 这个按钮就可以调用其他所有来源的补全项呢？要做到这点，可以使用 complete 这个配置项。该项包含一组由逗号分隔的单个字符表示的参数，当参数出现时表示需要扫描该参数代表的位置。使用 set complete? 可以看到，缺省项为 complete=.,w,b,u,t 。我们可以使用 set complete-=i 或者 set complete+=k 来删除或者添加某个扫描位置。常见的位置参数如下所示：. : 表示当前以打开的缓冲区w : 当前打开的窗口b : 当前缓冲区列表u : 当前处于缓冲区列表中，但是未打开的缓冲区t : 当前标签文件U : 当前打开的，不属于缓冲区列表中的缓冲区k : 从字典文件中加载的补全项i : 从当前文件和包含文件中读取d : 从当前文件和包含文件中读取使用 define定义的宏完整的内容可以使用 :h 'complete' 来查看。使用字典文件在上面的论述中，我们可以知道 vim 是可以自定义补全的字典文件，然后从字典中产生匹配的。我们可以使用 <Ctrl-x><Ctrl-k> 来加载字典中的匹配项。我们可以使用 set spell来启动拼写检查，拼写检查也会产生新的字典文件。如果不想使用该项，也可以使用 set dictionary来指定含有一个或者多个单词的字典文件。在这个例子中我们在 nvim-config 目录中新建一个 spell.txt 文件，我们在里面写入如下内容require return request然后使用 set dictionary=./spell.txt ，接着删除 init.lua 中的 return ，输入 re 然后使用 <Ctrl+x><Ctrl+k> 这个时候我们发现它已经加载了补全整行除了补全单词，vim还可以补全整行的内容，使用 <Ctrl+x><Ctrl+l> 可以触发补全整行的操作。补全行的补全项来源与补全单词相同，需要注意的是补全行的操作会自动忽略行首的缩进。补全行的操作与之前介绍的 yy 或者 :t 产生的效果相同，我们应该要根据实际情况分别使用。补全文件名在 shell中输入命令可以使用 <Tab> 键来自动补全文件路径，vim中使用 <Ctrl+x><Ctrl+f> 来对文件路径和文件名进行补全。需要注意的是当我们使用相对路径来补全文件名时，使用的是工作目录，也就是你从哪个目录中进入的vim。我们可以在 vim中使用:cd来切换工作目录。例如我在 nvim-config这个工程的根目录执行 nvim init.lua，我们在这个文件中希望快速补全 basic/settings.lua这项，我们发现它在补全的时候报错这个时候我们可以使用 :cd lua 来切换工作目录到 nvim-config/lua 。这个时候再执行补全命令就可以了。根据具体编程语言生成补全上述补全在编辑普通文本的时候显的有点用处，但是作为程序员平时在写代码如果只能使用上述方式进行补全肯定会抓狂的。好在vim 提供了像其他IDE那样的基于编程语言的补全方式。使用该补全方式的快捷键为 <Ctrl+x><Ctrl+o> 要启用该方式，需要启动文件类型识别。nvim 中已经启用了这一特性，因此不必特意进行设置，但是这里我还是给出它的配置。vim.o.filetype="plugin"或者vim中可以使用如下代码set filetype=plugin set nocompatiable # 设置与vi 不兼容例如我们可以尝试着在 css 文件中使用补全vim 本身也确实支持很多语言的自动补全，但是为了获得完整的体验还是推荐使用各种专门的补全插件获得更好的体验最后的总结在这边文章中，介绍了vim中补全项主要的几个来源分别是：当前缓冲区和缓冲区列表、包含文件、外部程序生成的标签等等。同时也介绍了如何使用快捷键来进行不同项的补全，现在对这些快捷键总结如下：<Ctrl + n> : 普通关键字补全，主要来源自缓冲区列表和当前缓冲区<Ctrl+x><Ctrl+n> : 与 <Ctrl+n>作用相同<Ctrl+x><Ctrl+i> : 从包含文件中获取补全项<Ctrl+x><Ctrl+]> : 从外部标签中获取补全项<Ctrl+x><Ctrl+k> : 从字典文件中获取补全项<Ctrl+x><Ctrl+l> : 补全整行<Ctrl+x><Ctrl+f> : 补全文件名<Ctrl+x><Ctrl+o> : 根据编程语言来进行补全
之前介绍了vim中的搜索模式，使用正则表达式可以很方便的在一个文件中进行搜索。后续也介绍了如何使用 argsdo 命令在参数列表中进行替换操作。但是到目前为止还没有介绍如何在工程目录中进行搜索，而这个功能是其他编辑器的基本功能。vim 主要运行在 unix 平台，而 unix 平台信奉的哲学是专门的软件做好专门的事，在多个文件中搜索关键字是 grep 这个程序的工作，vim本身并没有单独提供类似 grep 的功能，而是提供了方法直接调用 grep。本篇我们将要讨论在vim中是如何调用 grep 进行搜索的。并且介绍其他搜索整个工程中代码的方式。提前声明一下，因为vim中 grep 命令与 shell中的 grep 重名了，容易造成误解，因此这里采用 :grep 来表示 vim 中的 grep 命令，grep 来表示 shell中的 grep，也就是 vim中的命令都以 : 开头grep 命令vim 中也提供了 :grep 命令，它是对 shell 中 grep 的封装。它可以让我们直接在vim中使用grep并且可以在vim中显示结果（当然我们也可以在命令模式中使用 :!grep 来调用shell的 grep 命令）。我们仍然以前面介绍的搜索 TODO 标签为例。我们先在 shell 中使用 grep 命令。grep -n "\-\- TODO" **/*.lua因为 --TODO 中的 - 在shell中是传参的标志，所以这里需要进行转义。-n 表示在输出的结果中显示行号。**/*.lua 表示在所有lua文件中进行搜索。我们可以看到，它输出了我们想要的结果。我们该如何根据这个结果快速跳转到对应位置呢？例如要跳转到 lua/basic/settings.lua 的第5行， 我们可以在 shell 中可以使用 nvim lua/basic/settings.lua +5 表示打开到该文件并跳转到第5行。当我们要频繁不同文件间进行跳转的时候，要频繁的退回到 shell 并执行 vim 来打开，操作上比较繁琐。vim 为这种需求提供了自己的工具—— :grep 命令和 :vimgrep在vim中输入 :grep "\-\- TODO" **/*.lua 会发现 vim 在下方显示了当前所有搜索到的内容。这里我们没有加上 -n 选项，但是它仍然显示了行号，vim默认自动为 grep 添加了 -n 选项。这些内容被存储在一个被称之为 quickfix 的列表中。可以通过这个列表快速跳转到对应的位置。遍历 quickfix 列表quickfix 列表是由我们执行 :make 命令或者 :grep 命令所产生的，它会保存一个或者多个文件位置信息。我们可以使用以 c 开头的一组命令来遍历，下面列举出相关的命令：cnext：跳转到下一项cprev：跳转到上一项cfirst：跳转到第一项clast：跳转到最后一项cnfile：跳转到下一个文件的第一项cpfile：跳转到上一个文件的第一项cc n：跳转到第你项copen：打开 quickfix列表cclose: 关闭 quickfix列表后续使用 vim 时会大量使用到 quickfix 列表，为了减轻输入的负担，可以考虑将其定义为快捷键。:cnext 和 :cprev 命令前面可以加数字表示向后或者向前跳转多少次。例如我这里使用 :2cnext 表示向后跳转2次。我们可以使用 :copen 来使用新的窗口来显示 quickfix 列表中的内容。在这个窗口中可以使用 motion 命令来移动光标。quickfix 列表无法进行修改，因此这里只能移动光标。它比较特别的一点在于，如果我们在某一行按下回车键，那么vim会自动跳转到光标所在行对应的位置。quickfix 所在窗口总有一项处于高亮状态，这个状态表示当前我们在访问哪个位置的内容，我们可以通过窗口跳转来改变高亮的行，执行 :cnext 和 :cprev 以及 cc 之类的命令也可以修改当前高亮的行。例如我在这里执行 :cc 2 来跳转到第二条记录另外 vim 会自动保存之前产生的 quickfix 列表，并不会随着执行新的 :grep 而发生覆盖。我们可以使用 :colder 来查看上一个列表，使用 :cnewer 来查看下一个。定制 grep命令vim 中的 :grep 是对 shell 中的 grep 的一个封装。前面说道，vim 中的 :grep 命令会默认加上 -n 这个选项，而 grep 还可以使用 -i 来忽略大小写，我想把这项也加入到 :grep 命令中该如何做呢？另外 :grep 是对 shell 中的 grep 的封装，现在我有更好的文本搜索工具，我想用它来替换 grep 该如何做呢？还有一个很奇怪的点，在使用 :grep 进行搜索的时候，我们明明输入的是 :grep "\-\- TODO **/*.lua" 但是它给我们显示结果的时候显示的却是 :!grep -n "\-\- TODO" **/*.lua /dev/null 2>&1| tee /tmp/nvimPRHF8B/6 这是为什么呢？在这一小节我们将来探讨这些问题。当我们通过 vim 来执行 :grep 命令的时候，grepprg 负责制定将要调用的 shell 命令。grepformat 决定如何来 :grep 命令的输出结果。通过使用 :h grepprg 和 :h grepformat 看到，它们自身在 vim 中的默认值如下:grepprg = "grep -n $* /dev/null" grepformat = "%f:%l:%m,%f:%l%m,%f %l%m"在 grepprg 中 $* 表示占位符，它将被 :grep 命令中输入的内容替换，这也就解释了为什么最后在显示的时候，会在我们输入的基础之上加上了后面那些内容。 我们只需要对其做一些修改就可以使我们的 :grep 自动忽略大小写set grepprg=grep\ -n\ -i\ $*我们看到，同样的命令现在多出来了一条小写的结果，另外从它的显示上看也已经加上了 -i 选项了。接下来我们来看看 vim是如何解析 :grep命令输出的。grepformat中各种匹配格式是按照 ,来进行分割。也就是它定义了多组可能的输出格式，每组以 ,分割。%f表示文件名称、%l表示行号，:m表示匹配的行。了解这些之后，我们来试试使用别的命令来替换默认的 grep。这里我们以 ack作为演示，当然你也可以使用其他的命令。插一句题外话，我觉得 ack相较于 grep来说，最大的优势在于它可以识别不同的文件类型，这样就可以做到只搜索某一类型文件中的内容，而且默认支持递归搜索当前目录下所有文件。在 shell 中，可以直接使用 ack "\-\- TODO" 来搜索所有的 todo项，也可以使用 -i 来忽略大小写。在默认情况下 ack 会用两行来显示搜索到的结果，第一行是 文件名，第二行是行号和匹配行的内容。ack 默认会搜索当前目录中所有文件中的内容，所以这里可以不需要像 grep 那样给出具体的目录。我们可以使用 --nogroup来达到与 grep相同的输出格式。我们可以使用 --nogroup 来使 ack 达到与 grep 相同的输出，因此这里也可以不修改 grepformat 的内容。我们只需要修改 grepprg 即可：set grepprg=ack\ --nogroup\ $*另外 ack 还支持添加 --column 来输出对应的列，配合 grepformat 我们可以做到精确定位到对应的行和列。这里我们设置 set grepprg=ack\ --nogroup\ --column\ $*。同时设置 set grepformat=%f:%l:%c:%m从上图中可以看到，此时已经可以显示列号了，并且 grep 已经被替换成了 ack 了vim 提供了很方便的方式让我们修改 :grep 命令的行为。但是我们在执行 :grep 的时候发现它在调用 ack 命令有时候会造成一定的疑惑或者误解。而且并不是每次我都想使用某一个 shell 程序的。例如这次我想用 grep 进行搜索，下一次我想用 ack 搜索，这样每次修改外部命令，我都得修改 grepprg 和 grepformat 想想也挺麻烦的。为什么不创建一个 :ack 命令专门用于使用外部的 ack，或者其他命令专门用于调用其他外部程序呢？目前很多插件都是这么干的。在后续介绍 vim配置的时候我们将会给出这样的例子。vimgrep 简介除了使用 :grep 来调用外部的搜索命令外，vim 自身也提供了 :vimgrep 命令。它最大的特色是支持 vim 自己的正则表达式。它的使用格式如下::vimgrep[!] /{pattern}/[j][g] {file}它的使用方式与之前介绍的 搜索模式类似。只是它只支持2个标志，j 表示不进行跳转只是将匹配结果保存到 quickfix 列表中，默认情况下，它会跳转到第一个匹配的位置，并且将搜索结果保存到 quickfix 中。g 表示将所有匹配都记录下来，默认只记录每一行第一个匹配处。因为它与搜索模式下使用的模式相同，因此这里我们可以先用查找模式来在一个文件中进行试验，试验成功后再使用 vimgrep，否则错误的结果将会污染历史的 quickfix 列表，影响后续使用 colder 和 cnewer 。例如这里我还是搜索 --TODO 可以现在单个文件中使用 :\v--\s+TODO进行搜索。然后使用模式域留空的方式查找，即 :vimgrep //gj **/*.lua关于 vimgrep的内容就介绍到这里了，一般我很少使用原装的 :grep和 :vimgrep。而是采用功能更加强大的其他搜索插件。各位小伙伴也不需要纠结究竟掌握它们中的哪个好，有更好的，直接用更好的就行。
在前面的文章中，我们介绍了如何进行查找和替换，而替换是建立在查找基础之上的一个简单的应用，它只是将匹配文本修改为另一个。那么vim中还能针对匹配上的文本做哪些操作呢？在本篇文章中我们来对这个问题进行探讨。初识global 命令我们能够对存在匹配项的行进行其他操作的关键在于 global 命令。global 命令的作用是存在匹配项的行上执行指定的ex命令。命令的格式如下::[range] g[lobal][!]/{pattern}/[cmd]与大多数ex命令一样，它接收一个作用范围。如果不给范围，则默认作用于整个文件，即它默认范围是 %! 代表取反，是在不存在匹配项的行上执行ex命令pattern 表示匹配模式cmd表示将在对应文本上执行哪些ex命令。如果不指定则默认执行 print命令这里需要强调的是，执行ex 命令操作的是有匹配项的行。操作的不是高亮的文本，而是有高亮文本的行。我们还是以一个简单的例子来演示如何使用#define VERSION "v1.0.1" char pszVersion[] = VERSION; #define TITLE "vim" char* pszTitle = TITLE; #define AUTHOR "Bram Moolenaar" char* pszAuthor = AUTHOR;假设有这么一段代码，我们先用 ".*" 来匹配一个字符串，然后执行 :g//d 来执行删除操作。我们发现它并不是删除了后面的字符串而是将所有有字符串的行都删除了，只保留了赋值语句。相信通过这个例子各位小伙伴应该已经理解global 命令是如何作用的。如果我们要删除上述代码中所有的赋值语句，可以利用 !来进行取反，:g!//d在 《vim 实用技巧》这本书中提到一个很有意思的东西。通过上面的描述，可以总结出 global命令的一个简写形式 :g/re/p 其中 g是 global命令的缩写，re代表正则表达式 regular expression, 而 p 则是 print的缩写。我们将 / 从中去掉就发现这个简写变成了 grep 这个单词。这也就是 grep 这个命令的由来。上面的内容已经初步介绍了 global 命令的使用，下面再来看看其他的使用场景配合缓冲区参数列表使用我们还是用 neovim 的配置文件。我们随机在部分 lua 文件中加一些 TODO 的注释。表示暂时未做将来会实现的功能。我们先在某个文件中查找 TODO字样，有的文件显示没有找到也不要紧，只是为了保存这个模式然后将所有的 lua 文件加入到参数列表中，:args **/.*lua然后选择清空一个寄存器 qaq ，其中 qa 代表我们将要使用 a 寄存器来录制一个宏，不输入任何内容直接使用 q 结束录制。因为宏就是将操作内容写入寄存器，所以不进行任何操作的宏就可以清除寄存器的内容。然后执行 :argdo g//yank A 这里使用 yank 这个命令来复制内容到寄存器。另外使用了 A 而不是 a 因为这里是对每个文件依次执行命令的，需要一个个的添加到寄存器里面。所以这里使用大写字母。此时可以查看 a 寄存器的内容，发现已经有对应内容了。提取出来的内容有一个缺点就是无法显示具体是哪个文件中的 todo 项。将未来要实现但是现在没实现的功能用 TODO 描述出来在编程中是一个很常见的习惯，针对这个功能有许多做的不错的插件，后续将会介绍相关插件。指定 ex 命令的执行范围不光 global可以指定范围，后面接的 cmd也可以指定范围，下面将通过一个演示该如何使用假设有一段 css 代码html{ margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } body{ line-high: 1.5; color: black; background: white; }我们想对每组css内部属性按字母顺序进行排序。第一个办法：可以录制宏来自动化。首先通过模式来匹配 { 即使用 /{ 然后开始录制宏： nvi{:sort ，首先通过 n 来跳转到下一个匹配，然后使用 vi{ 通过文本对象来选中 {} 中的内容，最后针对选中来执行 sort 命令但是我们可以使用 global 命令完成同样的操作。首先还是来构造对应的模式。我们可以通过 :g/{/ .+1,/}/-1 sort 这么一条命令来完成这一操作。我们来分析一下这条命令的意思。首先将这条命令按照 / 分为3个部分，第一部分是 { 表示匹配所有 { 之后的内容。第二部分是 .+1,/}/-1 。它表示一个范围，范围分为两个部分，以 , 分割，前面一部分代表的是当前行的下一行，也就是{ 所在行的下一行，/} 表示匹配结束的 } 符号，后面跟一个 -1 表示 } 所在的上一行，这个范围代表的就是 {} 之间的所有行。最后一个部分是命令也就是 sort，在对应的这个范围中执行 sort 命令。针对上面解释的内容，我们可以对 global命令再做一个详细的使用格式：:[range]/g[lobal][!]/{start pattern}/ .{offsize},/{end pattern}/{offsize} [cmd]这里我们也可以跟其他命令，例如将C函数中的代码进行缩进，那么就可以使用 :g/{/ .+1,/}/-1 > 。
之前讨论了关于在vim中使用正则表达式的相关知识能方便的进行搜索，现在在之前的基础之上继续来讨论如何进行替换操作。substitute 简介substitute 允许我们先查找一段文本并用新的文本将匹配上的文本进行替换。它的使用比较复杂，需要提供一个匹配模式和一个替换的字符串。命令格式如下：:[range]s[ubstitute]/{pattern}/{string}/{flag}range 表示范围，与之前介绍的其他 ex 命令中范围的作用一样。pattern 表示一个匹配模式，回忆一下之前说过的，这里的模式跟之前介绍的普通模式、插入模式的含义不同，它代表的是一串用来进行匹配并高亮显示的字符串。string是一串用来进行替换的字符串，将匹配项都替换成某项。flag是一些替换的标志，我们将在后面的内容中进行介绍。例如 :%s/python/Python/g 表示在整个文件中将 python都替换为 Python（这么长时间没怎么提到 ex 命令了，不知道各位小伙伴是否还记得 %代表当前打开的文件）。g 是一个标志位，表示修改整行中的所有匹配项，而不仅仅是修改第一个匹配项。标志位上面的例子中我们使用了一个 g 作为标志位，其实还有其他的标志位。我们可以通过标志位灵活的定义 substitute 的行为。下面是一些常用的标志位：\r：插入一个换行符\t：插入一个制表符\\：插入一个反斜杠\1：插入第一个子匹配项\2：插入第二个子匹配项\0：插入匹配模式的所有内容&：与 \0用法相同~：使用上一次调用 substitute时提供的 string内容\={vim script}：执行 vim script并将返回内容作为 string有这么多标志位，该怎么记，平时怎么用到呢？别急，下面将通过相应的示例来演示如何使用它们，我们完全可以在日常使用中学会它们。示例使用 g 替换所有内容how can I learn python very good, just use it more and more. python is very powerful, you can just learn python within your work.我们将上面文本中的所有 python 都改为 vim 。我们先来看看不使用标志是什么样子的。即这里输入 :%s/python/vim我们发现它只替换了每一行的第一个出现 python 的地方，同一行后面的 python 不受影响。这里我们使用 /g 替换每一处出现 python 的地方。g 这个标志很容易联想到 global 这个单词，应该表示的是整个选中的文本范围，而我们前面已经选定了当前文件中的所有文本，似乎看起来很合理。但是 g 作用范围应该是整行，而我们选中的是文本中的所有行。看起来效果是一样，但是理解起来确实有差距。为什么它会作用于行，我想应该是 vim 发源于 ed 这个编辑器，vim的 ex 命令起源于 ed 编辑器，而后者是一个行编辑器，所以大部分的命令都作用于行。这样应该就能说得通了。手动选择是否需要替换有的时候我们并不希望盲目的对所有内容进行替换，而只替换其中的部分内容。例如上述的文本中，我们只想替换第二行的最后一个 python 为vim。那么可以使用 c 标志。你可以理解为 copy ?。vim会询问我们是否需要进行替换。即我们在这里输入 :%s/python/vim/gc。后面可以按下 y 来确定替换，n 表示不进行替换并切换到下一处匹配。因此这里我们可以输入 nny其实不光yn这两个选项，从vim的提示看总共有 ynaql和 以及 。它们的含义如下：y：替换本处匹配n：不替换本处匹配a：替换此处之后的所有匹配项，随后退出本次替换q：退出本次匹配l：替换此处之后退出本次替换：向上翻滚屏幕：向下翻滚屏幕重用上次匹配模式如果我们将 {pattern} 部分留空，那么 vim会重用上次的 {pattern} 。下面我们使用一个例子来看如何使用这一特性。#define VERSION "v1.01" #define TITLE "vim" #define PATH "~/.config/nvim"我们想将里面的字符串改为宽字符，也就是在双引号前加L我们首先要匹配所有的引号内容。很多时候正则表达式比较复杂，无法一次就写对的，所以这里我们分步骤来，首先正确写出正则表达式匹配出所有带双引号的字符串。 \v\"(.*)\"可以获取所有的字符串。接着我们使用上面的这个模式来进行替换，即输入 :%s//L\0/gc。当然这里只有这么三行一眼就知道我们要替换所有，但是代码一长了，就需要我们来确认是否需要替换。复杂的正则表达式我们无法一次就输对，如果进行替换操作的时候因为正则表达式输入不对导致每次都得重新输入一堆内容就显得比较麻烦了。而且如果使用 substitute 命令之后才发现错了，又得撤销重新输入那么大一串。与 substitute 相比，查找模式不会修改文本，我们可以在查找模式中使用 <Up> 慢慢修改直到满意为止。当模式对了，下面就可以利用 {pattern} 留空这种方式来重用上次模式。需要注意的是将模式留空，将会在历史命令中留下一个不完整的记录，模式与命令是独立存储的。在上面的例子中，如果我又执行了新的匹配，例如我想查找所有 define，后面使用 重新执行命令的时候，发现匹配的内容变了。有一个办法就是将上次的模式存储到寄存器中，在匹配的时候从寄存器中取数据填充 {pattern} 部分。上次匹配成功之后如何将对应的模式放到寄存器呢，这里我们介绍一个新的内容——命令窗口。命令窗口是一个显示历史命令的缓冲区，它跟普通的缓冲区区别仅仅在于它显示的是历史命令而已。使用 q: 可以调出，这里我们可以使用 q/ 调出模式的命令窗口。在对应模式行使用 "iy$ 来粘贴一行，然后在最后替换时使用 <C-r>i 来填充 {pattern}。使用留空这一招有时候很方便有时候又很麻烦，上述两种方式具体如何使用请各位结合具体情况自行判断吧。使用寄存器的内容进行替换{pattern} 域留空了，vim会自动以上一次的模式来进行匹配，那么如果我把替换域留空，是不是会以上次替换的字符串作为这次的进行替换呢？试验过后发现 vim 并不会这样做，它会使用空字符串进行替换（单纯的使用上次的替换字符串使用的是 ~ 这个符号）。如果想要快速填充替换域，可以先进行复制，然后在 substitute 中使用0 寄存器。即，我们可以输入 :%s/{pattern}/<C-r>0/gc 来完成替换。就想上面的例子那样。但是这种方式有一种缺陷，那就是如果复制的内容中有 / & 之类的特殊符号的话，它会出现错误。这个时候我们可以手动编辑寄存器中的内容，对特殊符号进行转义。这个时候我们会想有没有什么办法能让vim知道我只想将寄存器中的特殊符号作为普通字符串呢？当然是有办法的，我们可以借助vim script 来实现这一需求。我们可以输入 :%s/{pattern}/\=@0/gc，其中 \=是我们之前列举的使用 vim script，而后面的 @0 则是 vim script 的内容，表示取 0 寄存器的内容。这里出现了 vim script的内容，不过不用担心，这里涉及到的都是最简单的vim script内容，而且更新完了 vim 的基础内容之后会开一个新专栏介绍vim script和vim的配置，那个时候再回过来看这个方法也可以。这段脚本也很容易理解是不是？使用上一次的 substitute 命令假设我们在执行 substitute命令的时候忘记了在前面添加 %，我们当然可以使用 <Up> 键来在上一次的基础之上进行修改。这里介绍一个更简单的方式，可以在普通模式中输入 g& 它会在整个文件中重新执行上一条 substitute命令。它等效于 :%s//~/&。当你其他部分都正确，唯独忘了添加 %可以考虑使用这条命令。我们再来看另外一种情形：假设我有这么一段代码mini = { applyName: function(config){ return obj.factory(config, this.getName()); }, }我要在此基础之上增加一个新函数，使其变为这样mini = { applyName: function(config){ return obj.factory(config, this.getName()); }, applyNumber: function(config){ return obj.factory(config, this.getNumber()); }, }我们发现，只需要简单的将上一个函数进行复制，然后将 Name 改为 Number 就可以了。我们可以使用 :%s/Name/Number/g 来执行替换，但是有一个问题就是它将所有的内容都进行了一个替换。好在可以使用一次 u来撤销所有修改。在介绍命令模式的时候介绍过，大部分的 ex 命令都可以使用选择模式中选中部分作为命令执行的范围，substitute 同样可以。我们先选中后面要更改的部分，然后使用 :&&来在选中部分重复执行上一次的 substitute 命令。这两个 &具有不同的含义，第一个 & 表示重复上次执行的 substitute 命令，但是它不包含上次指定的标志位，在后面再加一个 & 表示重复上一次的标志位。后面的 &与上面介绍的 g&中的 &含义相同。使用子匹配进行替换假设我有这么一份联系人记录Jack Ma, 12398988011 Pony Ma, 16528649018 Donald Trump, 15092838173 Joe Biden, 18571820986现在我想改变一下顺序，让电话号码在前，人名在后。首先构造一个可以准确匹配到人名和电话号码的正则表达式： (.*),\s+(\d{11}) 。第一个括号匹配的是人名，第二个括号匹配的是电话号码的11位整数。然后我们可以利用之前介绍的 {pattern} 留空的方式，重新组织新的排列格式 :%s//\2, \1使用 vimscript 脚本在上面介绍从寄存器中读取内容进行替换的时候初步介绍了，使用 vimscript 的例子。这里我们再举出一个使用 vimscript 的例子，不过不用慌，使用的脚本都极为简单，不存在理解障碍。假设有这么一段HTML代码 this is a h2 tag this is a h3 tag this is a h4 tag this is a h5 tag 我们希望对标题标签进行升级，换句话说就是将 ，提升为 ， 提升为 。首先我们构造模式来匹配对应的数字，可以用 \d 来匹配数字，但是它会匹配到所有数字，因此我们加一个限定，只匹配以 <h 或者 </h开头的数字，这个时候正则表达式可以改为 \<\/?h\d，我们只想要后面的数字，因此可以对这个匹配进行裁剪，\<\/?h\zs\d。这样就匹配到了所有标签后的数字，但是内容里面的数字没有被匹配。接着我们介绍一个新的 vimscript 命令——submatch，它接收一个表示第几个匹配的参数，返回对应的匹配项。我们可以使用这个函数将获取的每个匹配项都 -1，即输入 :%s//\=submatch(0)-1/g 就可以完成这个操作了。最后的总结在这篇文章我着重讨论了 substitute 这个命令的使用，介绍了该命令对应的标志位，并通过一些例子演示了如何使用这些标志位。相信各位对替换命令有了一定的认识。各位小伙伴可能还会有疑惑，目前介绍的查找替换似乎只针对的是某个文件，如果我想在项目中进行全局替换该怎么办呢？请各位想想之前我们是如何在多个文件中执行宏的。这部分就不做介绍，算是留的一个练习吧。至于多个文件进行查找，我们将在后面的部分继续介绍。
上一篇文章中，我们初步结识了如何使用查找模式，也能够通过n和 N进行查找。这篇将会介绍搜索中更高级的用法。另外在写上一篇文章的时候我发现介绍查找相关内容的时候不能用动图来演示，主要是因为输入的内容太多了，剪成动图的话太大了，不一定能上传。第二个就是开启了匹配高亮的选项，比起动图来能更直观的看到匹配的结果。所以这篇文章就不采用动图了。调整大小写敏感默认情况下，在搜索时是大小写敏感的，例如下面的例子中，我们无法匹配到大写的REQUIRE的可以使用 ignorecase 项来取消大小写敏感，例如在 neovim 中写上如下配置vim.o.ignorecase = true或者在 vim 的配置文件中写上set ignorecase此时在输入 require 时，发现已经可以匹配到 REQUIRE 了但是在实际使用中我们希望有时候大小写敏感，有时候大小写不敏感，例如想模糊搜索某个函数或者变量的时候。更好的做法是设置 smartcase 项。它只有在输入的字符中有大写才启用大小写敏感，否则就是大小写不敏感。vim.o.smartcase = trueset smartcase当然我们也可以在每次搜索的时候单独指定本次搜索是否大小写敏感。可以在匹配时输入\c来不区分大小写而使用 \C区分大小写，这个符号可以出现在任何位置，哪怕你输入 /requ\Cire它也能正确找到所有的 require字符串。使用正则表达式匹配vim支持正则表达式的搜索，vim采用的是 POXIS的正则表达式的规则，这就让我们一些习惯 Perl正则表达式规则的人在使用时会出现一些不适宜。现在我们来看一个具体的例子假设现在有一个 css 文件, 我需要搜索里面的16进制颜色body { color: #3c3c3c; } a { color: #0000EE; } strong { color: #000; }我们在搜索时输入这样一个正则表达式 #([0-9a-fA-F]{6}|[0-9a-fA-F]{3}) 。发现它会报错，但是正则表达式来看，这么写是没问题的，我们要匹配的是以 # 开头，后面有6个或者3个16进制数的字符。这是因为里面有特殊字符，需要进行转义，例如 () 在 vim 中有特殊用途,我们将在接下来介绍它的用途。我们需要将正则表达式写成这样 #\([0-9a-fA-F]\{6\}\|[0-9a-fA-F]\{3\}\) 好家伙，反斜杠居然有7个，而且 ()、{} 需要转义，而 [] 不需要转义。正则表达式就够麻烦的了，还得记住vim与其他编辑器的不同，用一次人就麻了。好在vim提供了 very magic 模式，即除了 _ 、 数字、字母之外的所有字符都具有特殊含义，这样我们就不用纠结哪些需要转义，哪些不需要了。可以在搜索的开头添加 \v 来启用这一模式，即我们可以输入 \v#([0-9a-fA-F]{6}|[0-9a-fA-F]{3}) 我们还可以使用 \x 表示16进制数，以便简化上述正则表达式, \v#(\x{6}|\x{3})。\x 是vim 字符类中的一个成员，我认为比较有用的还有如下这些字符含义\x十六进制数\X非十六进制数\d数字\D非数字\o八进制数\o非八进制数\w包括字母、数字和 _\W不包括 字母、数字和 _\h包括 字母和 _\H不包括字母和 _\l小写字母\L非小写字母\u大写字母\U非大写字母除了有 very magic模式，vim 中还有 very nomagic 模式，在该模式中所有的字符都只表示它自身，没有特殊含义，例如 . 在正则表达中表示单个字符，但是在 very nomagic 模式中它就表示一个点，它可以匹配点这个字符本身，要启用 very nomagic 模式，可以使用 \V 作为前缀。使用括号获取子匹配项在 vim 中可以使用 <(\w+)\_s+\1> 来匹配重复单词，例如I love python python is so good这句话中我们可以匹配到 python 这个单词。我们来看这个正则表达式，<> 匹配以某些字符开头或者结尾的单词，例如 <Py 将匹配所有以 Py 开头的单词，而 on> 将匹配所有以 on 结尾的单词，因为这里我们的需求并没有要求要匹配以某些字符开头的单词，加上这个就限定我们要匹配单词而不是某些个字符。\_ 后面可以加vim字符类中的任意字符，代表在原来的基础之上额外再匹配换行符，例如 \x 可以匹配16进制数，\_x 可以匹配16进制数和换行符。\_s+就是匹配 <Tab> 空白字符和换行符，并且是匹配1个或者多个。在vim中使用括号代表子匹配项，它是整个正则表达式匹配的一个子项，例如 Py(tho)n 它可以匹配到 Python 和 Python 字符串里面的 tho。\后面加数字代表第几个匹配项，第0个匹配项是整个正则表达式的匹配项，1、2、3、....、n 则对应着第1个子匹配项，第二个、第n个子匹配项。了解了这些，我们就能读懂整个这个正则表达了，它匹配这样一个单词：他是任意单词，但是它后面需要出现一个跟他一样的单词，不管中间包含的是多个空格、制表符或者换行符。如果我们只是想匹配是否有多个重复的 Python可以这样写： (<Py\w+on>)\_s+\1界定匹配范围在搜索模式中，vim把查找域中输入的内容（可以是正则表达或者是原意匹配的字符串）和它匹配的到的高亮的文本进行了区分。一般将查找域中的内容称之为模式，将被高亮显示的文本称之为匹配。一个模式可以对应多个匹配（这里的模式与前面提到的普通模式和插入模式的意思不同）。一个匹配的边界通常对应着一个模式的起始与结尾。例如 <> 表示一个匹配的边界将是一个单词。除了这个，vim提供了 \zs 和 \ze 这两个元字符来对一个匹配进行裁剪。\zs 表示去掉匹配中开头的指定部分、\ze 表示去掉匹配中结尾的指定部分。例如在上面这句话中，我通过 <\w+> 匹配到所有的单词。然后通过 <\zsPy\w+> 来对匹配内容进行裁剪，将高亮显示所有单词 Py 以及后面的内容，如果不是以 Py 开头的则完全被裁剪掉了。或者使用 <\w+\zeon> 来裁剪，只显示所有单词 on 前面的部分。转义特殊字符这里我们用一个URL 作为例子来演示https://www.baidu.com/search?q=\\/假设我们要匹配所有文档中出现的这个url，该如何做呢？首先考虑在匹配模式中输入一大串的内容，但是这一大串不需要手工输入。我们可以将vim的命令模式和现在的匹配模式看成一个特殊的插入模式中的文本，这样我们就可以使用前面介绍的在输入模式中使用寄存器的例子。先使用 "iy$ 粘贴一行，然后在匹配模式中使用 <Ctrl+r>i 来粘贴。但是这个时候我们发现匹配的结果并不是我们想要的，这是因为在匹配模式中 / 是具有特殊意义的特殊字符，我们需要告诉vim将其解释为普通字符，这个时候可以使用 \V 来进入 very nomagic 模式，该模式与 very magic 相反，将所有字符作为普通字符来解释。我们会返现它只匹配到了 https:，并且模式中的字符串也变成了 https: , 后面从/开始截断了，这时候我们可以使用 \/ 对 // 进行转换。同时 \ 本身也作为特殊字符，我们也需要对其进行转义。即整个匹配应该输入 https:\/\/www.baidu.com\/search?q=\\\\\/本篇中主要讲述了如何在vim中使用正则表达式，到此应该已经聊完了vim中查找模式中的基本操作了。后面我们将介绍该如何进行替换操作。
最开始介绍vim的时候，提到vim有普通模式、插入模式、可视模式和命令行模式，并且已经对这几个模式做了详细的介绍了。除了这几个模式以外，vim还有一个非常强大的模式——查找模式，为什么最开始没有将其列举出来呢，这是因为我很少看到有教程将它与前面介绍的模式并列作为一个新的模式。但是在日常使用中，我又习惯将它叫做查找模式。我在这里就不深究这些了，请读者自行判断。插入模式使用我们可以在普通模式下输入 / 进入插入模式。在其后面输入想要查找的字符串，按下<CR> 进行查找。此时vim光标会自动跳转到匹配的位置，并将对应位置高亮显示( 这个是 neovim的特性，vim高亮可能需要一些配置)。使用 <Esc> 将会结束查找，退回到普通模式。在匹配模式中，可以使用 n 来跳转到下一个匹配位置，按下N 将跳转到上一个匹配位置。这个查找是循环进行的，也就说在跳转到最后一个匹配位置之后，再使用 n 将会跳转到第一个匹配位置。我们可以使用 wrapscan 这个选项关闭这一特性，例如在neovim 中使用如下代码vim.o.wrapscan = false或者在vim中使用这样的代码set nowrapscan在查找模式中，可以使用 <UP> (也就是方向键) 来遍历之前查找项。取消高亮的匹配vim 中可以使用 hlsearch 来设置将匹配项进行高亮显示( neovim 则默认支持该项) 。高亮显示匹配项这个特征在我们寻找这些匹配项的位置时十分有用，但是一旦找到想去的地方之后，这些高亮显示的内容却会干扰我们的视线，特别是匹配项过多的时候。这个时候我们可以使用 noh 来取消高亮。但是每次这么输入比较麻烦，我们可以绑定一个快捷键，快速取消高亮。这里还是等到介绍配置的时候再介绍吧。执行前预览第一处匹配在一般的vim中，一定要等到输入完完整的内容，然后按下 <CR> 键之后才会进行匹配，而执行前预览会在每次有新输入的时候更新匹配。类似于下面的效果。输入 s 的时候，所有 s 字符都被选中，输入后面的内容时再次匹配，只高亮匹配上的。vim模式并未开启这个效果，可以使用 incsearch 来激活这一效果。而neovim则默认支持这一选项。快速选中光标所在单词进行搜索这里主要为了介绍 *这个命令，当光标处于目标单词的时候，按下 * 将直接进行搜索。将光标移动到匹配的结尾默认情况下，使用匹配模式时，光标都在匹配字符串的开头位置，我们可以在搜索结尾处添加 /e 来让vim将光标移动到匹配字符串的结尾处相信通过这篇文章各位小伙伴已经初步了解了如何进行查找，但这些内容是远远不够的，vim提供了强大的搜索处理能力，在下一篇我们将慢慢展开介绍。
终于到了我第二喜欢的vim功能了（当然了，最喜欢的是.命令）。我原本计划在介绍完.命令之后介绍宏，以便让各位小伙伴们能了解到vim对于重复操作进行的强大的优化。但是由于宏本身跟寄存器息息相关，所以还是忍痛割爱，将它放到寄存器之后。废话不多说让我们开始吧。宏的基本使用我们还是以一个例子进入相应的内容。我要将下面这段代码foot = 'foot' ball = 'ball' football = foot + ball变为这样var foot = 'foot'; var ball = 'ball'; var football = foot + ball;通过分析它这三行其实做的都是同样的功能，即在每行的行首添加 var 关键字，然后在每行的行尾添加分号。我们可以利用之前介绍的.命令或者针对列的可视模式来处理，只是不管用哪种方法都需要至少两次操作。问题先放在这里，让我们先了解下什么是宏，以及怎么用宏。宏是存储在寄存器中的连续的操作指令，以便后续可以对这些指令进行回放。可以使用 q 进行录制，后面跟寄存器名称，表示将接下来的操作记录保存到这个寄存器中。例如使用 qa 表示将接下来的操作保存到 a 这个寄存器中。退出宏的录制可以直接输入 q针对上面的例子，我们可以执行 qa 进行宏的录制，然后使用 A 在行尾进入插入模式，接着输入 ; 完成行尾的操作。然后使用 I 进入行首，然后在行首输入 var 完成这部分的工作。最后使用 q 退出宏的录制。这样就将这个宏保存在了a 寄存器.我们可以使用 :reg a 来查看寄存器的内容。这个内容完全是我们之前通过键盘输入到vim中的内容，只是返回到普通模式输入的是<Esc> 而这个保存的是^[ 因为宏有自己的键盘编码方式，这个方式我觉得不需要特别去查去记，自己就可以从寄存器中查到。宏录制完成之后，可以使用 @ + 寄存器 来回放寄存器中保存的宏。在回放宏之后可以使用 @@ 来快速回放上一次回放的宏。到现在各位小伙伴可能已经发现了，它与.命令比较类似，只能机械的执行之前执行过的内容，它无法做到智能化，例如我在录制宏的过程中使用了诸如 2w之类的命令，后面在重复的时候很有可能发生错误。这就要求我们在使用宏的过程中，尽量规范化光标移动，不要搞这种特例的形式。就像写代码不要写死一样。这里我们还是手动执行了好多次同样的宏，宏与普通的operator 一行支持前面加数字表示重复，例如2@a 表示重复执行两次这个宏。上面的例子我们可以稍微做一下修改，即在最后添加一步将光标移动到下一行的操作——j。然后使用这个特性进行重复。仔细点可以发现，我们执行了3次这个宏，也就是要执行3次j 操作，但是我们是在第二行执行的宏，也就是剩下的行只允许我们执行一次j 。这里虽然有问题，但是宏还是正确的对文本进行了修改。这是因为 vim 宏在 motion 执行失败之后会终止执行，这个并不是一个 bug，而是一个特性，也就是说利用这个特性我们可以更好的使用宏。例如上述例子中，宏只执行了一次 j ，第二次执行到j 的时候出错了，于是就停下来了。这就告诉我们不用关心剩下的操作需要重复多少次，只需要给出一个足够大的数，保证已有行能正常进行修改就可以了。我们再来看一个例子将1. one 2. two 3. three 4. four 5. five 6. six 7. seven 8. eight 9. nine 10. ten改为1) One 2) Two 3) Three 4) Four 5) Five 6) Six 7) Seven 8) Eight 9) Nine 10) Ten我们可以这么归纳这个操作，从行首开始找第一个 .，然后执行替换操作将其替换为 )然后找到下一个单词，将首字母改为大写。我们可以在宏中执行 0f.r)w~j最后退出。读一下这段内容， ~之前没见过吧。之前介绍过，gU和 gu后面可以跟 motion表示将对应范围的字符转化为大写和小写。g~可以进行大小写反转。而这里的~直接将当前光标所在字符进行反转。上述命令我们首先使用 0将光标至于行首，这样就规范了每行的查找操作。另外这里由于 10 有两个字符，所以这里使用 f 来查找而不仅仅使用 l往后移动一个字符，最后我们不确定. 和单词之间会不会有空格。所以这里最好是使用 w 而不是 l 。这些细节体现了我们之前说的要更加规范的移动光标。宏录制完了之后，我们可以利用之前介绍的 motion失败会终止执行的特性，不用数需要处理多少行，直接 10@a（因为第一行已经处理了，所以这里只有9行待处理）以并行的方式执行宏我们将上述例子进行变更1. one 2. two 3. three 4. four // do something 5. five 6. six 7. seven 8. eight 9. nine 10. ten执行上次录制的宏，发现它在第5行的位置停止了，因为在第5行中未找到 .，所以它终止了，为了继续运行，需要手动跳过，然后继续执行。假设我们有多处有注释，每次遇到问题就停下来，再手工执行，会显得比较麻烦。为了解决这个问题，我们使用vim提供的另外一种执行宏的方式——以并行的方式执行。重新录制宏，与之前相比，只需要将j这个操作给去除掉。然后使用针对行的可视模式，选中待处理行，然后针对这些选中行来执行宏。我们在这里来审视一下这两种方式，并行方式需要提供重复次数，它是第一次执行完了接着执行下一次，下一次的执行依赖于上一次成功的执行。并行则不然，并行是针对选中部分，同时执行一个宏操作。即使中间有错也不影响其他行的运行。给宏追加命令还是上面的例子，假设在录制好了宏之后发现我们少了一个j，使用串行话的方式无法顺利执行。这种情况下不需要重新录制宏，只需要在对应寄存器中添加一条指令。这里补充一下寄存器相关知识。在上一篇介绍寄存器的时候我们只演示了使用小写字母的寄存器，没有提到大写字母的寄存器。根据之前的惯例，大写字母与小写字母都可以使用，大写字母的功能比小写字母要强，例如大写的标签标示全局，小写的只能用于单个文件。这里大写的寄存器与小写的寄存器是同一个寄存器，使用大写时我们可以对寄存器内容进行追加操作。宏是保存在寄存器中的，q 后面加字母表示宏的内容保存在哪个寄存器中，说到这里，聪明的你已经反应过来该如何将命令追加到寄存器中了。那就是使用 q+大写字母。针对并行操作的例子，假设已经录好其他操作只差一个j 了，我们可以使用 qA 进行追加，然后添加 j 操作即可追加前宏的内容如下： 添加完成之后，宏变成了如下内容后面就可以以串行的方式执行这个宏了配合文件参数列表使用宏之前介绍过文件参数列表，即使用 :args 可以对文件进行分组，各位小伙伴可能只知道这个，但是没找到它的使用场景。也不知道vim提供这个功能有什么用处。在这里我们就来看看它的一个使用场景。我们还是以之前的 neovim 配置文件的工程为例，我要在每个lua文件中添加一行注释 --this is add by vim macro 。打开一个 lua 文件之后，使用 :args **/*.lua 来将每个 lua 文件加入到参数列表中。然后随意打开一个 lua 文件，在录制宏的时候执行 ggO<ESC>S--this is add by vim macro 然后退出。这里还是贯彻了前面说的要是移动更加规范，我们先用 gg 移动到第一行，以便能准确的在首行插入内容。由于在 lua 文件中有注释的话使用 O 添加一行的时候它会自动添加一个注释。但是不能确保所有的 lua 文件在行首都有注释，所以我们先使用 S 删除一行并进入插入模式。当然通过配置也可以取消这个特性，等介绍到文件类型的时候再来讨论这个。此时文件已经发生了变化，如果我们直接执行宏的话，之前录制时修改的文件将会两次执行相同的命令，所以这里不能保存，可以执行 :edit! 放弃本次修改，或者如果已经修改了的，可以执行u进行回退。结合之前介绍的在命令模式中执行普通模式的命令，可以使用 :argdo normal @a 。argdo 表示循环对参数列表中的每个文件执行相同的操作。录制宏:添加参数列表：执行宏上述的操作方式采用的是并行的执行宏，我们可以对其进行一些修改，让其支持串行的方式。还记得之前介绍的怎么遍历参数列表吗，不记得也没关系。我们可以使用 :next 来访问下一个，:prev 来访问上一个。配合之前的命令可以使用 ggO<Esc>S--this is add by vim macro:next。我们无法知道参数列表中到底有多少个文件，但是可以利用失败即终止这个特性输入一个足够大的数字即可，例如 100@a即可。这样就省去了执行命令模式中命令的相关操作。对比两个宏发现我只需要在之前的宏后面添加一个 :next 指令即可，所以这里就直接执行了 qA:nextq对比上面两种方式发现，并行执行的时候中间某个缓冲区如果出错并不影响其他缓冲区的执行，这就给我们排查造成了一定的问题，一旦出错我们不得不打开每一个缓冲区查看执行的结果来找到出错的位置。而串行则会停在出错的位置，我们只要针对出错的部分做一定的调整，然后继续执行就好了。而且这个例子中列表参数并不会循环遍历，也就不用担心之前修改过的内容又被修改。编辑宏内容上面我们说到宏是保存在寄存器中的一组操作指令，既然可以利用往寄存器中追加内容的方式往宏中追加指令，那么是不是我只要更新了寄存器中的内容，在执行宏的时候命令就会改变呢？如果你能这么想，那么恭喜你都会抢答了，而且答对了！还是以上面那个添加注释的例子为例，假设我之前忘记了删除新添加的 --，也就是我录入的宏变成了 ggOthis is add by vim macro 我们会发现在第一行是注释的文本中它的表现是正常的，但是第一行不是注释，添加的就是有问题的，例如 nvim-config/lua/config/auto-session.lua。我们发现了这个问题需要对这个宏进行修改。首先我们需要将 宏从寄存器中放到编辑器中，这就要使用 :put a取出寄存器中的内容，你可能会疑惑为什么不用 "ap 呢，这是因为 p 命令默认会将寄存器中的内容放到光标所在位置的后面，而 :put 则会直接放到下一行，所以这里还是放入到当前命令之后要好。接着修改一下这个宏。在对应位置加上 S 这个操作，最后使用 0d$ 从行首粘贴到行尾，注意这里尽量不要使用 dd，它会连带着换行符一块进行粘贴，可能会破坏宏的指令。最后我们可以先删除之前粘贴的一行，再重新执行这个宏最后的叨叨宏是vim提供的很有用的功能，希望我通过本文让各位小伙伴对它有一个初步的认识，想要用好宏这个强大的工具还是需要花大量的时间去学习研究的。vim这个工具也是常用常学常新的，时不时你就能发现自己当初不知道的内容，就像有小伙伴给我留言给我介绍了一些我之前不知道的命令，在这里对所有给我留言的小伙伴表示感谢。vim的指令实在太多了，指望我把所有好用的一一介绍，文章的篇幅就显的太长了，这里我就不加了，各位小伙伴有什么好用的方式也可以留言给其他不会的小伙伴一个学习的机会。大家一起共同进步。谢谢大家
在计算机里面也有寄存器，计算机中的寄存器是看得见，摸得着的实体，寄存器中存储需要经常访问的一些数据。而vim中也有寄存器的概念，vim中的寄存器是一个虚拟的概念，更像是一块专门用来存储数据的内存缓冲区。在使用vim的过程中离不开寄存器，而且我们很早就用到了寄存器，只是没有发现罢了。这篇文章将深入介绍寄存器，这样我们对之前使用的命令将会有更深的认识。几种寄存器类型无名寄存器在之前介绍过，可以使用d来删除一段内容，使用p来粘贴，使用y来复制， vim中其他的带有删除功能的operator 像 x、c、s 之类的，vim在删除之前会将被删除内容先放到无名寄存器中，然后执行删除操作。严格意义上来说，他们并不是删除而是剪切。后续可以通过p 命令来粘贴之前被删除的内容例如print("hello world") print("")改为print("") print("hello world")就可以在第一行通过di"，将被删除的内容存储到无名寄存器中，然后在第二行对应位置执行 p (或者P) 命令取出无名寄存器中的内容。这里在使用h 移动光标之后，光标所在位置在后一个引号的位置，为了减少一次光标移动，我直接使用P 在光标所在位置之前进行粘贴操作。这里插一个题外话，vim中的命令都是某些有意义的单词的首字母或者几个字母的缩写，像d代表delete、y代表yank，那么p又代表什么呢，粘贴的英文是paste，但是知道寄存器以及复制粘贴在vim的表现，我觉得应该是put，就是将寄存器中的内存拿出来。好在他们的首字母相同，不影响记忆。有名寄存器它是对应无名寄存器来说的，无名寄存器虽然说使用方便，但是有一个很大的问题，那就是如果我们连续两次执行了删除或者复制操作，那么前一次保存的内容将会被后一次的给覆盖掉。为了解决这个问题，一个思路就是使用有名寄存器。vim中提供了由a到z的有名寄存器，可以在使用operator 的操作前面指定需要使用的寄存器，引用一个寄存器可以使用 " + 寄存器名的格式。这个时候我们之前的公式就又可以扩展了" + regester + operator + motion例如在执行删除的时候 "add 将一行删除的内容放到a寄存器中，再次执行"bdd将内容放到b寄存器中，执行粘贴的时候，可以使用"ap和 "bp来分别使用 a和b寄存器的内容。无名寄存器有一个专用的符号，使用 "来表示。也就是说dd 命令其实等效为 ""dd。但是为了偷懒和方便，还是少输几个字符的好。复制寄存器前面说到使用 dd 之类的命令会将被删除的内容放到无名的寄存器中，它的行为有点像普通编辑器中的剪切，那它是不是剪切呢，那么多教程都把它叫做删除，是不是有问题呢。它确实是删除指定，教程说的也没错，vim中有专门存储复制内容的寄存器。普通的删除命令会把被删除的内容保存到无名寄存器中，但是这些内容不会被保存到复制寄存器中。复制寄存器使用 0来表示。即我们可以使用 "0p来将复制寄存器的内容取出。也可以通过命令 :reg 0来查看这个寄存器的内容。在上述例子中，我们先在第一行执行 yy 操作进行复制，这个时候数据会被同时保存到无名寄存器和复制寄存器。然后在第二行执行 dd 删除，这个时候第二行的数据会被保存到无名寄存器，之前保存的第一行的数据就被删除了，但是不会被保存到复制寄存器，第一行的数据仍然存储在赋值寄存器。接着立即执行p 它会从无名寄存器中中取出我们之前删除的第二行的数据。接着再执行"0p 会从复制寄存器中取数据，这个时候取出的仍然是第一行的数据。黑洞寄存器前面说到 dd会将被删除内容放入到无名寄存器中，如果这段内容我确实不想要了，也不想它占用寄存器，有没有什么办法彻底删除呢，答案是使用黑洞寄存器，顾名思义，放入该寄存器中的内容都被吸走丢失了，无法使用了。黑洞寄存器使用 _作为标识符，执行删除指令的时候可以使用 "_dd这样就再也访问不到之前删除的内容了。在上面的例子中，我们先执行普通的dd 命令，它会将删除内容放入到无名寄存器中，第二次执行的是 "_dd ，它会将内容放到黑洞寄存器中直接丢弃，后面执行p 的时候只会粘贴第一次删除的内容。有小伙伴可能会说，这个跟我使用其他有名寄存器效果是一样的。看不出什么区别，那么我们可以试试使用 :reg 命令来查看有名寄存器和黑洞寄存器的值，我们发现黑洞寄存器的值永远为空，而有名寄存器此时多了一条记录。系统剪切板之前我们在vim中复制粘贴的内容，只能在vim中使用。同样的系统中复制粘贴的内容只能在系统其它程序中使用，无法直接粘贴到vim中。我们可以在vim中使用系统剪切板。vim可以使用+来访问系统剪切板。例如使用 "+yy将内容复制到系统剪切板中，供其他程序使用。但是在有好的shell工具的加持下，我更喜欢用<Ctrl+v>这样的方式直接粘贴一大段文字到vim中。或者配合vim的可视模式，直接使用shell中的快捷键从vim中粘贴选中的内容到系统剪切板表达式寄存器前面介绍的几种寄存器都是被动的存储静态的内容，只有存储功能。表达式寄存器则可以接受一段vim脚本并执行它并输出结果。表达式寄存器使用 =来表示。例如在插入模式中可以使用 <Ctrl+r>=6*6 来进行数学计算并输出。到此已经介绍完了vim中几种主要的寄存器，是不是觉得挺抽象的，而且用起来也不是那么方便。下面我们还是以例子来展示相关用法。示例示例1：复制粘贴的使用假设我们要将如下代码print("hello world") print("hello vim")修改为print("hello world") print("hello world")可以在第一行使用 yi"来复制引号内的内容，但是如果在第二行先使用 ci"再使用 p进行粘贴的话会发现粘贴的仍然是hello vim这是因为后面使用c操作的时候，已经将原来的给覆盖掉了。现在我们来解决这个问题。第一种解法：y指令可以额外产生一个动作，它会将内容放入到复制寄存器中，那么在执行了ci"之后使用 "0p来使用复制寄存器中的内容第二种解法：可以手动指定ci"删除的内容放入到黑洞寄存器中，即执行"_ci"这样就不会产生覆盖问题。前两种方法虽然解决问题了，但是都引入了新的寄存器，按键比较繁琐，如果不想引入新寄存器，就得使用接下来介绍的第三种方法了，它也是我最喜欢的方法了。解法三：由于我们需要先删除之前的内容再复制，为了快速删除，所以会发生覆盖问题，我们只要不执行删除操作就不会覆盖了，为了一次性完成粘贴替换的操作，可以使用选择模式，之前介绍选择模式的时候说过，在选择模式下operator 会将选中部分作为操作区域。可以使用 vi" 来选中引号内容，然后直接使用 p 完成复制示例2：插入模式中使用寄存器之前已经在介绍表达式寄存器的时候已经介绍了如何在插入模式中使用寄存器，可以使用 <Ctrl + r> + register 例如上面的例子可以使用 <Ctrl + r>0来将复制寄存器中的内容写入到光标所在位置。相比上面介绍的前两种解法，它省去了切换回普通模式的步骤，相对来说更高效一点。但是我觉得它还是比不上第三种解法。示例三： 处理粘贴混乱的问题有的时候在粘贴代码的时候发现代码的格式可能会乱，这个时候可以设置 :set paste选项，设置之后复制粘贴将不再出现这个问题，但是它会使我们在其他模式下设置的快捷键失效，在粘贴完了之后可以使用 :set nopaste取消该设置。由于我使用的neovim中没有发现这个问题，就不演示了，各位使用vim的小伙伴可以自行测试。当然处理该问题的另一种方式是使用之前介绍的在vim中使用剪切板的方法，也就是使用 "+p这样的形式。
之前介绍过众多的motion，根据移动范围来排序的话有 l、e、w、j等等，但是面对那么长的代码文件，仅仅使用这几个简单的motion不知道要移动多少次才能找到我想要的代码，这个速度有时候还不如我用鼠标移动光标。vim作为编辑器之神当然提供了快速移动光标的方式了，这篇文章我们就来了解一下如何使用vim在代码间进行快速跳转。利用标签，快速跳转vim中提供了标签的方式进行跳转，事先可以在对应位置设置标签，后面通过标签访问该标签所在位置可以使用m{a-z} 来在任意位置设置标记，而后使用`{a-z}来回到对应标记位置。该命令可以回到之前设置标签时光标所在行和列。vim可以支持从a到z的26个位置标记，一般来说我们用不到这么多，即使你能全部用到，可能早就忘了前面标记的在哪个位置了。这种方式有一个最大的问题就是在标记之后从显示上无法知道我们的标记位于何处。除了由用户主动使用m 来设置位置标记以外，vim还会自动为我们设置标记，例如上次修改、上次跳转、上次高亮等等。下表列举出了，如何回到这些vim自动标记所在位置位置标记含义\`\`当前文件中上次跳转动作之前所处的位置\`.上次修改的地方\`^上次进入插入模式的位置\`[上次修改或者复制的起始位置\`]上次修改或者复制的结尾位置\`<上次高亮选区的起始位置\`>上次高亮选区的结尾位置在匹配的括号间进行跳转可以使用 % 在一组括号中使用，可以跳转到下一个匹配的()、[]、{}。例如下列操作我们可以配合operator 来使用，删除括号中的内容。例如下面的代码var foo = { "obj":{ "test": "1", "arr": [1, 2, 3] } }可以使用将光标移动到对应的位置，然后使用d% 就可以删除对应的内容了。当然也可以使用文本对象来进行跳转列表浏览器中会记录浏览历史，并且提供了去到上一页和下一页的功能。vim中也提供了类似的功能，vim会记录我们每一次的跳转，可以通过相关命令来跳转到上一次跳转和下一次跳转的位置。我们先介绍什么是跳转。跳转似乎很容易理解，似乎光标每次的移动都算是一次跳转。但是vim中的跳转并不是这样的。我们可以先这样理解，motion 允许我们在一个文件中进行移动。而跳转则是不同文件间的移动。就像在浏览器中从一个页面打开另一个页面。为了类比浏览器的操作，你也可以把每次跳转记录理解成历史访问文件的记录。可以使用:jumps 来查看跳转记录。从上图中可以看到这样几个现象：跳转列表中记录了所在文件以及上次光标所在的行和列。最后几行由于我们处在当前文件中，所以没有列出文件名称来，而是直接给出光标所在行的文本内容，由于我这里打开文件之后立即查看了跳转列表，光标处于第0行这个虚拟行，所以会显示空白内容。它记录了光标所在的行列，所以后面我们在恢复的时候可以直接定位到具体位置。与浏览器类似，之前打开vim的时候访问文件的记录也在里面，它并没有随着vim的关闭而被清除。任何能改变当前窗口中活动文件的命令都可以作为跳转命令，像find、edit 之类的。了解了跳转列表之后，我们现在来访问一下这个跳转列表。可以使用<Ctrl + i> 来访问前一个跳转，<Ctrl + o> 来访问后一个跳转。在nvim-config 中随意打开一个文件，然后使用edit 打开另一个，接着就可以使用<Ctrl + i> 和 <Ctrl + o> 在两个文件中切换了我们再来联想一下浏览器中的历史记录，我们发现有时候访问同一个页面的不同位置可能会产生多条历史记录。例如访问同一页面的不同锚点。那么我们之前说的将跳转理解为历史文件访问记录可能就不对了，同一个文件也可以产生多个跳转记录。 例如gg(G)、%、\{a-z}等等。而h j k l w f之类的就不作为一次跳转。用一句话来总结就是大范围的光标移动才会被作为一次跳转。什么会被作为大范围的移动呢？我个人的理解是一次移动有能力移动至少半屏，而像50j 之类的虽然也可以移动50行，也达到半屏以上，但是前面加数字表示的是重复，它是重复了多次，并不算一次移动。我们使用 split 或者 vsplit 再打开一个新的窗口，然后在两个窗口中分别使用:jumps 发现二者并不相同。vim可以维护多套跳转列表，每个窗口都有自己的一套独立的跳转列表。这个与浏览器中的也类似，新窗口并不能进行前进和后退操作，而且只两个操作也只能跳转到由这个窗口打开的网页上。改变列表回忆一下，我们不管在文件的哪个位置，使用u撤销修改的时候光标总能跳转到对应修改的位置，或者使用\. 能回到上次修改的位置。如果以前没有注意这个细节的，也可以现在试试。vim在会话期间会维护一张表，表里记录了每个缓冲区的每一次修改。这个就是所谓的改变列表。可以使用:changes 来查看这个列表这个列表与跳转列表类似，都标记了行号与列号。我们可以通过g; 和 g, 来访问下一个和上一个记录。你可以拿;和, 来类比记忆。这两个操作符是配合f来使用的。; 移动到下一个匹配位置，, 移动到下一个匹配位置。我们可以使用\.来跳转到上一次修改的位置，而 `^则更具体一点。它代表的是上一次退出插入模式光标所在位置。如果我们在做出修改并且退出插入模式之后，移动光标查看了下其他类似代码的实现，然后想快速回到之前编辑的位置继续编辑，可以使用 `^将光标移动到对应位置，然后使用i进入插入模式，当然也可以使用gi 一步到位，直接从上次编辑位置进入插入模式。需要注意的是，vim会为每一个打开的窗口维护一个跳转列表，但是更新列表只有一个，而且跳转列表并不会随着vim的退出而消失，但是改变列表则会随着vim的退出而被清空。跳转到光标下的文件在我们将当前项目所在的所有路径加入到path中之后(即在项目根目录中执行:set path+=./**)可:set 以将光标移动到对应表示相对路径的代码上，执行gf 即可跳转到对应文件。在上面的例子中，我们只写了settings 这样的文件，它是如何知道要打开 settings.lua 文件的呢，或者说如果有类似的settings.h 或者 settings.js 在同一个位置的话，它该打开哪一个呢？比如说我们新建一个settings.h 在同样的目录中，再次执行之前的操作，发现它还是能够正确的打开settings.luavim 中有一个suffixesadd 变量，它保存的当前缓冲区中执行gf操作时，可以使用的扩展。我们可以像设置path 一样，例如:set suffixesadd+=.java 来允许打开java文件。使用gf 也是一个跳转，也会被记录到跳转列表中，后续我们可以使用之前介绍的<Ctrl + o> 和 <Ctrl + i> 来回的在两个文件中切换。使用全局书签在文件间跳转之前介绍过在文件中可以使用标记，在文件不同位置进行跳转。那个时候说到使用小写字母设置标记，小伙伴们可能会产生疑惑，那大些字母去哪了呢，为什么只能使用小写字母，而大写字母被空着呢？文章写到这里了，我可以告诉大家，大写字母被用到了全局书签里面。全局书签与之前介绍的标记使用方式一模一样，只是一个使用大写字母，一个使用小写字母。例如在上面一个例子中，我们在跳转到settings.lua 之前先使用mI 在init.lua中一个标签，在跳转之后使用 `I 快速跳转回来。好了，本篇内容就到这里了。下一次将介绍寄存器相关内容。再次感谢大家的阅读