Vim宏快速入门

2021-10-15 ⏳5.4分钟(2.2千字)

Vim提供宏录制功能,可以记录一系列操作然后重复执行。这个功能比较冷门,却也非常强大。只是因为操作复杂,很少有人学,也很少用。但我认为宏的思想很简单,之所以觉得复杂是没有特别好的实战案例来体现它的价值。我今天在修改Sniper框架的时候就遇到了一个比较适合使用宏的场景。现在整理出来分享给学习和使用 Vim 的朋友。除了宏之外,本文还向大家展示了复制粘贴、行内查找、临时插入模式、寄存器等功能综合运用,特别适合初级和中级 Vim 爱好者。

Sniper框架需要封装 viper 库并提供一系列工具函数。这些函数都是简单的调用 viper 对象对应的函数,函数签名完全一样。我在 Vim 中用 tagbar 查看代码结构,一次性从 tagbar 中把需要封装的 viper 复制出来,内容如下:

   +GetBool(key string) : bool
   +GetDuration(key string) : time.Duration
   +GetFloat64(key string) : float64
   +GetInt(key string) : int
   +GetInt32(key string) : int32
   +GetInt64(key string) : int64
   +GetIntSlice(key string) : []int
   +GetSizeInBytes(key string) : uint
   +GetString(key string) : string
   +GetStringMap(key string) : map[string]interface{}
   +GetStringMapString(key string) : map[string]string
   +GetStringMapStringSlice(key string) : map[string][]string
   +GetStringSlice(key string) : []string
   +GetTime(key string) : time.Time
   +GetUint(key string) : uint
   +GetUint32(key string) : uint32
   +GetUint64(key string) : uint64

现在我需要把 +GetBool(key string) : bool 修改成下面的样子:

func GetBool(key string) : bool { return File(defaultName).GetBool(key) }

对比一下就会发现,需要把前面的空白和+替换成func,删除中间的冒号,最后插入一段函数 body。这里最关键的是 body 的内容跟前面函数名是对应的(比如这里的GetBool)。整个替换的逻辑完全相同,但每一行要处理的内容又不一样。这种情况就特别适合用宏来处理。下面我就分析一下如何操作。

首先开始录制,按qa。这里的a表示寄存器编号,可以取a-z,回放宏的时候要用到。按了qa之后,接下来所有的操作都会被记录下来。

先按0,跳到当前行的开始位置(^只能跳到第一非空白字符)这一步非常关键,后面部分会揭秘😄。

然后按cf+,通过行内查找f+选中从开头到+这片区域,c表示清空选中的区域并进入插入模式。按完就会发现+和前面的空白都删除了,这个时候输入func,然后回到普通模式。到现在,我们改好了开头部分。此时光标停在func后面的空格上。

下面我们添加函数体。因为在函数体中需要用到当前的函数名,所以我们先复制到剪贴板。按w跳到函数名开头,再按yiw复制整个单词,i表示不包含单词。单词的边界可以是空格,也可以是其他一些标点符号,比如这里的(。按完之后,GetBool就会保存到剪贴板。

在追加函数体之前,我们还需要删除中间的冒号。先把光标移动到冒号上,按f:,然后按"_dw。注意,这里先按了"_,再按的dw删除冒号。为什么不直接按dw呢?Vim默认会把当前删除的内容保存的剪贴板,如果直接按,会覆盖刚才保存的函数名。而这里的"_表示黑洞寄存器,也就是说按了"_告诉 Vim 这次不要保存删除的内容(冒号)。

最后,我们直接按A,表示$a,其实就是先跳到行尾,进入插入模式(注意ai的区别)。这个时候就可以输入{ return Func(defaultName).。到这个时候需要一点黑科技了。接下来我们需要输入当前了函数名。最简单的办法是回到普通模式,按p执行粘贴操作,再按A继续输入。但有点太啰嗦了。

这里的本质需求是要在插入模式下从剪贴板粘贴内容。有两种思路。

第一种是直接插入指定寄存器的内容。这个需要先按CTRL-r再按对应的寄存器编号。Vim默认会把复制的内容存入编号为"的寄存器(你没看错,是英文引号)。所以要在插入模式下粘贴复制的内容可以按CTRL-r Shift"。这种方式可以用于任意寄存器,不局限于复制粘贴,大家要活学活用。但考虑到寄存器本身不常用,所以这种方法不太推荐。

另一种是使用所谓的Insert Normal模式,我们不妨称之为临时普通模式。在插入模式中按CTRL-o可以普通模式,你按任意普通模式下的快捷键。但只能按一次,Vim执行完对应的操作之后会自动切换回插入模式。如果需要复制,可以按CTRL-op,然后继续输入}完成全部改动。

这里的Insert Normal模式非常好用。比如我们在插入代码的时候想快速移到到一行的开始,可以按CTRL-o^,如果单词写到一半发现写错了,想重新输入,可以按CTRL-ocw等等。建议大家熟练掌握。

最后,返回普通模式,按q,结束宏录制。到这里,准备工作就结束了。现在我们回放刚才录制的操作。

j移到到下一行。因为光标在上一行的结尾,所以移动后还是在当前的结尾。这就是我需要在宏录制的时候先按0跳到行首的原因。然后按@a,执行寄存器a中保存的宏记录。你会发现+GetDuration(key string) : time.Duration瞬间被改成了:

func GetDuration(key string) : time.Duration { return File(defaultName).GetDuration(key) }

重复按j@a就会依次修改每一行。如果觉得按@a有点麻烦,还可以在首次运行后按@@,这里第二个@表示上一次使用的寄存器,也就是a

最后我们可以看看寄存器a中到底保存了什么内容,可以执行:echo @a,结果如下:

0cf+func ^[wyiwf:"_dwA { return File(defaultName).^Op(key) }^[

这里的^[表示esc,大家可以对照前文看看这些内容,其实就是我们在按qaq之间按过的所有键!

执行一次宏,不管修改了多少内容,都是原子操作。也就是说你可以按u来撤销修改,也可以再按CTRL-r反向撤销。

其实 Vim 还会自动录制宏,记录最近的一次修改操作。比如删除一行可以按dd,这个会被 Vim 自动记录。重复执行这个宏不需要指定寄存器,可以直接按.。大家可以试试。

以上就是就本文的全部内容。宏虽然有点复杂,也不是很常用,但宏的功能很强大,实现方式也很简单,建议所有 Vim 同学熟练掌握。

最近需要执行复杂的查找与替换操作,也是用宏来实现的。有兴趣的可以阅读这篇文章

感谢@Yu Fei, @gkzhb同学留言,为我们提供数字+@a的方式,可以进一步提高宏的使用效率:

不过这篇好像没有提到 数字 + @a 可以重复执行宏多次。比如录制操作到 a 寄存器之后可以重新录制一个操作到寄存器 b:@aj。即执行一次宏 b 会先执行一次宏 a,然后移动到下一行。这样我们可以通过 [n]@b 重复执行 n 次宏 b 来快速修改 n 行的内容([n] 替换为实际数字)。

更便捷的做法应该是在录制宏操作的结尾处加上“移动光标到下一个执行宏操作的位置”的操作,这样我们就不需要录制两个宏而只要一个宏就可以自动执行重复操作多次。