《Vim实用技巧》笔记

2020/12/30Wednesday Vim

📕《Vim实用技巧》, 以前想看只是因为评分挺高的,没想到居然写的这么好


. 命令可以用来重现上次的修改

一次修改的定义:

  • 普通模式 : 普通模式下的一次修改"操作"
  • 插入模式 : 进入插入模式使用的操作符 + 直到退出插入模式为止所输入的内容

进入插入模式的一些语法糖有特殊好处, 比如想要在每一行的末尾添加一个分号, 如果使用普通的$a;<ESC>, . 命令重现的上次修改就是操作符+输入内容=a;, 但使用A;<ESC>, .命令重现的上次修改就是A;

基础模式

普通模式

操作符 + 动作命令 = 操作

其中动作命令(motion)用来指定操作的范围, dl(删除字符l), daw(一个完整单词aw), dap(一整个段落ap).

常用操作符:

命令 | 用途

  • | - c | 修改 d | 删除 y | 复制到寄存器 g~ | 翻转大小写 gu/gU | 转换大小写 ! | 使用外部程序过滤 motion 所跨越的行

vim 的语法只有一条额外规则, 即当一个操作符命令被连续执行两次时, 他会作用于当前行. cc, dd, yy

插入模式

进入插入模式的一些语法糖:

  • s = cl
  • S = ^c
  • C = c$
  • I = ^i
  • A = $a
  • o = A<CR>
  • O = ko

WARNING

照理说 D = d$, Y 应该等于 y$ 才对, 但实际 Y = yy...

插入模式下可以使用以下组合键

按键操作 用途
<C-h> 删除前一个字符
<C-w> 删除前一个单词
<C-u> 删至行首
<C-j> 下一行

这些命令不是插入模式独有的, 在vim 的命令行模式中, 甚至 shell 中都可以使用

  • <C-o> : 切换到插入-普通模式(执行完一个普通模式操作后再次进入插入模式)
  • <C-r>{register} : 插入寄存器中的文本

在 vim 中使用/<C-r>"<CR>可以搜索复制的字符串, 对于需要转义的文本后面会介绍完整方案

TIP

表达式寄存器=, 可以用来执行一段 vim 脚本, <C-r>=4\*60\*60 插入模式下写入14400

可视模式

可视模式用于选中一块文本区域, 可在后续对其执行操作

激活可视模式的三种子模式:

  • v 面向字符的可视模式
  • V 面向行的可视模式
  • <C-v> 面向列的可视模式
按键操作 用途
gv 重选上次的可视区域
o 切换高亮选区的活动端

关于书中所讲的可视模式.执行的问题, 简单来说就是vitU命令中是先选择范围再执行转换, .只复制了转换过程只能改变选中字符相同的长度. 而普通模式下的gUit是一条完整指令使用.可以完全重现.

选择修改列文本, vim 基本的c r A I 可以用, 更复杂的功能还是推荐插件吧 https://github.com/mg979/vim-visual-multi

实战

将下列文本转成 markdown 格式的表格

Chapter            Page
Normal mode          15
Insert mode          31
Visual mode          44

首先将光标放在首行合适的分割位置再执行:

  1. <C-v>jjjr|将当前列替换成|字符
  2. yyp复制一个首行的副本
  3. 接下来需要用到命令行模式: :s/[^|]/-/g将副本行的非|字符替换成-
Chapter      |     Page
-------------|---------
Normal mode  |       15
Insert mode  |       31
Visual mode  |       44

TIP

<C-v>jjjr|yyp:s/[^|]/-/g 这段命令只是模仿书上的, 但例子我改了. 简单的方法应该是先写第二行的分隔符yypVr-, 再将光标移动到想要分割的列后: <C-v>jjjokr|

命令行模式

vi 的前身是名为 ex 的行编辑器

使用:可以执行各 ex 命令, 插入模式下的各种按键操作在命令行模式下同样试用

执行范围

print 简写p, 直接执行将会打印当前行内容

很多 ex 命令都可以指定一个执行范围, :2,3p将会打印2-3行的内容.

可用于指定范围的特殊符号:

符号 地址
1 文件的第一行
$ 文件的最后一行
0 虚拟行,位于文件第一行上方
. 光标所在行
'm 包含位置标记m的行
'< 高亮选区的起始行
'> 高亮选区的结束行(可视模式下将自动附加)
% 整个文件(:1,$ 的简写形式)

copy 简写 t

  • :6t. 将第6行的内容复制到当前行下
  • :t6 将当前行的内容复制到第6行下
  • '<,'>t0 将选中块中的文本复制到文件开头

move 简写 m 操作如上

@: 可以重复执行上次的 ex 命令

normal 简写 norm 在指定范围上执行普通模式命令

  • :%norm A; 向文件中所有行末尾附加分号
  • :'<,'>norm I//; 选中行添加//注释, 在可视模式下使用:将会自动附加'<,'>范围

normal 指令还是很常用的(可惜 ideaVim 暂不支持...)

在 vim 命令行下执行<C-r><C-w>将会复制光标下的单词并将其插入到命令行中, 普通模式下常用的*指令将等效于/\<<C-r><C-w>\><CR>

WARNING

<C-r>说明是个寄存器操作但<C-w>这是啥...

历史命令

  • <C-p> 上一个命令
  • <C-n> 下一个命令

这个和 shell 是一样的

  • q: ex 命令行窗口:h cmdwin

关键是命令行窗口, 相信很多用 vim 的小伙伴都误入过这种模式:

普通模式下执行q:后在左下角将会打开这个记录历史命令的窗口, 在该窗口内可以执行完整的 vim 指令, 这样就可以方便我们编辑/合并历史命令了

退出方法有两种:

  1. 使用 vim 的:q
  2. 在任一行按下<CR>即回车键将会自动退出并执行该行的命令, 而且初始行为空也方便我们误入该模式后直接<CR>安全退出

IMPORTANT

后面会介绍一个也很有用的查找记录命令行窗口 q/, 操作都是一样的

执行 shell

在命令行模式中, 加一个!前缀就可以调用外部命令 :!go run %

read/write前缀可以将当前 buffer 内容作为外部命令的标准输出/输入

最常用的是直接用外部命令过滤缓冲区内容, 只要在调用外部命令前给出指定范围就行了

%!cat a.txt: 使用 a.txt 的内容重写当前文件

john,smith,john@example.com
drew,neil,drew@vimcasts.org
jane,doe,jane@example.com

:%!sort -t',' -k2: 使用外部 sort 根据姓氏重新排序上面的内容

书中文件一章跳着看的, 自己全是用插件来做 buffer 管理

更快的移动和跳转

实际行与屏幕行, 默认的 hjkl 是根据实际行来移动的, 所以在超出屏幕宽度显示为多行中使用 jk 移动会出现反直觉的情况, 可以使用gj, gk命令按照屏幕行移动光标或直接 nnoremap 映射过去

单词移动:

将大写的W,E,B理解为快速移动就好了

F,T理解为反向查找

在执行d/nnore,y/nnore这类查找操作时候, 删除和拷贝的都是当前位置到查找位置nnore之前的字符不包括查找字符, 可以在后面接/e后缀来表示执行到查找位置末尾

个人最离不开的插件就是vim-easymotion(ideaVim 也有的), 并且将其指令前缀设置为;, ;原生是用来重复上一次行内字符搜索的, 需要多次查找的情况强制自己用 easymotion 慢慢就熟悉了

文本对象选区

i 可以理解为 inside, 指文本选区内部, a 理解为 around

文本对象 选择区域
a)/ab (parentheses)
i)/ib (parentheses)
a}/aB {parentheses}
i}/iB {parentheses}
at <xml>tags</xml>
it <xml>tags</xml>
aw 当前词及一个空格
iw 当前词
ap 当前段落及一个空行
ip 当前段落

前面接操作符c=>ciw,v=>vip等组成想要的操作

i 和 a 的选区算是我最喜欢并且入坑 vim 的操作了

位置标记

m命令后接选定字母可以用该字母标记当前位置, 比如mm可以记录当前位置到 m 标记上

跳转标记:

  • `m 跳转 m 标记的准确位置
  • 'm 跳转 m 标记所在行的第一个字符处

因为自己用的是60%键盘, ` 需要组合键触发, 更倾向于将这两个键互换下

vim 的自动标记位置:

位置标记 跳转到
`` 上次跳转动作之前的位置(当前文件中)
`. 上次修改的地方
`[ 上次修改或复制的开始位置
`] 上次修改或复制的结束位置

% 在匹配括号之间跳转

<C-o>,<C-i>文件级的位置来回跳转

快速跳转(语法糖)

  • g; 跳转到上次修改的位置
  • gi 恢复上次的插入模式

寄存器

vim 中复制粘贴命令xp,yyp等默认使用的都是无名寄存器"

指定寄存器 a 使用"前缀: "ax: 复制字符到 a 寄存器, "ap: 粘贴字符

无名寄存器总是缺省的, 常常会被一些意外的内容覆盖, 比如可视模式下粘贴, 总之用多了就后知后觉然后会设置复制专用寄存器 0 的快捷键, 比如: nnoremap <Leader>p "0p

特殊寄存器:

寄存器 内容
"= 表达式寄存器
"% 当前文件名
". 上次插入的文本
": 上次执行的 ex 命令
"/ 上次查找的模式

在可视模式下使用p命令时, vim 将用我们指定寄存器的内容来替换高亮选中的文本, 这里无名寄存器 " 将会被原高亮区文本覆盖.

在插入模式下使用<C-r>{register}可以插入指定寄存器的内容

  • <C-r>0 : 插入复制专用寄存器的内容.

宏允许我们把一段修改序列录制下来, 用于之后的回放

  1. qa 开始录制保存到 寄存器 a 的宏
  2. q 结束录制
  3. @a 播放宏

宏的使用虽然非常简单, 书中主要大篇幅讲了宏录制的规范

实战

将以下文本转成 markdown 的有序列表

partridge in a pear tree
turtle doves
French hens
calling birds
golden rings

使用let关键字可以在 ex 命令中创建变量:

:let i=0
:let i+=1
:echo i

前面提到在插入模式下引入表达式寄存器, 执行<C-r>=i<CR>可以插入变量 i 的值

  1. :let i=1<CR> 定义编号变量 i
  2. qa 开始录制宏并保存到寄存器 a 中
  3. I<C-r>=i<CR>. <ESC> 在行开头插入编号.
  4. :let i+=1<CR> 递增编号
  5. q 结束录制宏

播放宏可以使用两种方式:

  • 使用可视模式选中后面的行 jVjjj :这里自动附加 ex 执行范围为选中区 '<,'>norm @a 使用 norm 对选中的每行播放宏
  • 或者录制宏的时候在第4步干脆把j(即移动到下一行)也录制进去, 然后使用4@a手动播放
1. partridge in a pear tree
2. turtle doves
3. French hens
4. calling birds
5. golden rings

编辑宏内容

:put a 可以将寄存器 a 中的内容粘贴至当前光标行下方

0"ay$dd 可以将编辑后的宏安全复制回寄存器 a 并删除当前副本

模式

vim 中/命令按正则表达式查找时, 使用\v前缀可以开启 very magic 模式, 使得正则运作更像是一般编程语言所用的 Perl 风格

  • vim 原生正则风格(类似POSIX): /\(a\|b\) 查找 a 或 b 字符
  • very magic 风格(类似Perl): /\v(a|b) 查找 a 或 b 字符

捕获子匹配 /\v(a|b)\1 匹配 aa 或 bb, 这是编程中的常用操作, 后面介绍的用子匹配内容作为其他命令的执行范围非常有用

[warning] 不捕获子匹配内容用%()而不是一般编程的(?:)

匹配的边界

前面提到普通模式下的\*指令等效于/\<<C-r><C-w>\><CR>, 其中包裹着当前光标单词的<>符号是单词界定符表示这是一个单词的边界, 使用g*将不使用单词界定符

模式与匹配的区别:

  • 模式指查找域中输入的正则表达式
  • 匹配是文档中被高亮显示的文本

我们可以用\zs\ze对匹配进行裁剪, /v"\zs[^"]+\ze"将会查找引号所包裹的字符且不匹配包裹的引号

\zs,\ze都是零宽字符, 其功能和 Perl 正则中的"环视"即零宽断言类似. 上面例子的正则在一般编程语言中的写法是:(?<=")\[^"]+(?="), 术语叫做 零宽正向先行断言零宽正向后发断言

vim 脚本中的常用函数escape(参见:h escape)可以帮助转换字符, escape(@u, '\/'): 为寄存器 u 中所有 \ / 字符加上反斜杠前缀

查找

:set hls highlight search 开启高亮搜索

以前是看别人文章推荐将Backspace按键映射到:se nohls用来关闭高亮搜索, 书中推荐的方案是用<C-l>, 在原有的清屏重绘功能上加入关闭搜索, 也更符合键盘本位...推荐尽量用<C-j>来代替<CR>更别说<Backspace>

nnoremap <silent> <C-l>  :<C-u>nohlsearch<CR><C-l>

默认搜索的跳转位置光标会停留在搜索词的开始, 在某些脚本中想要搜索光标位置停留在搜索词末尾可以使用/lang/e, 这也是为什么需要转义 / 字符, / 可以用来标识结束匹配并追加自定义操作

q/ 命令行窗口(查找历史)

实战

查找所有单引号字符串并换成双引号

  1. /\v'(([^']|'\w)+)' 如果是代码防止的应该是反斜杠转义
  2. %s//"\1"/g 替换子匹配内容, substitute 命令后面会介绍

查找当前选中文本

vim 中没有默认提供该功能, 在可视模式下*命令还是查找当前光标下的单词, 我们可以自定义脚本提取高亮选区的文本后将其转义后再用于搜索

书中提供的配置:

xnoremap * :<C-u>call<SID>VSetSearch()<CR>/<C-R>=@/<CR><CR>
xnoremap # :<C-u>call <SID>VSetSearch()<CR>?<C-R>=@/<CR><CR>

function! s:VSetSearch()
  let temp = @s
  norm! gv"sy
  let @/ = '\V' . substitute(escape(@s, '/\'), '\n', '\\n', 'g')
  let @s = temp
endfunction

替换

substitute 语法 :[range]s[ubstitute]/{pattern}/{string}/[flags]

标志位 flags:

  • g 全局替换, 而不是默认的只替换第一个
  • c 手动确认是否替换(confirm)

替换域中的特殊字符

符号 描述
\r 换行符
\\\ 反斜杠
\1 插入第一个子匹配
\0 插入匹配模式的所有内容
\={vim script} 执行表达式

:%s/going/rolling/gc 全局替换整个页面的 going 为 rolling 并手动确认每一处

在上面例子中有用到%s//"\1"/g,替换前一次搜索子匹配的内容, 可以发现其中的查找域留空了, 当查找域为空时 vim 默认使用当前的模式, 所以上面的/\v'(([^']|'\w)+)'+%s//"\1"/g等价于:%s/\v'(([^']|'\w)+)'/"\1"/g

:&&执行上一次的替换行为, &快捷键将执行忽略标志位的上一次替换行为

global 命令

:[range] global[!] /{pattern}/ [cmd]

:global 简写 :g, 在某个指定模式的所有匹配行上分别运行 ex 命令

:g/re/p 其中的 re 指 regular expression(正则表达式), 这条命令的意思就是将该正则匹配的所有行打印出来(print), 这也是grep这个词的由来, ex 的前身 ed 首次使用了 grep 程序, 该程序最早由肯·汤普逊写成(没错就是那个设计 B语言, unix 和参与 Golang 开发的)

:v/re/d v 代表 inverse 反选, 该命令用来删除所有不匹配 re 模式的行

两个简单的例子:

  • :g/TODO/yank A 将包含 TODO 字符串的行添加至寄存器 a, 注意寄存器用大写表示追加写入
  • :g/TODO/t$ 将包含 TODO 字符串的行添加至文件末尾

来看一个匹配 css 文件中所有大括号内的属性并排序的例子

一个复杂的例子:

  • g/{/ .+1,/}/-1 sort 选中所有大括号包裹的行并排序

global 广义命令形式如下:

:g/{start}/ .,{finish} [cmd]

其中 +1 -1 都是偏移量, .+1,/}/-1表示从当前行+1到查询}所在行-1位置

IMPORTANT

在 vim 中遇到需要区分特定的某些行并执行操作时候就用 global 指令