vim使用技巧

 vim 󰈭 7606字

不知道怎么就放弃了VSC… 极为朴素的用了3个月Vim… 最近越发觉得不能够得心应手的进行编辑了, 因而感觉是时候系统地再精进一下Vim了

首先记一下VSC的一些问题…

  • 完全的OSS版本中, 许多插件都没有上线, 这样的VSC几乎无法使用了, 或许有什么奇技淫巧可以离线安装, 但是如果使用vsc还要折腾这么麻烦的话就偏离了使用它的初衷了; 而在微软的专有软件中(也是从官网上直接下载的版本), 插件有时候会有莫名的问题, 在我的archlinux上至少有如下两个问题, 感觉是比较大的瑕疵:

    • C/C++那个下载量最多的紫色插件, 好像偶尔会周期性的占满一个vCPU, 然后风扇开转… 感觉不爽

    • 在打开VSC的情况下关机, 偶尔会发生等待长达3min后vsc才被强制终结的情况(? 或发生了其余什么强制关闭事件, 我不清楚), 这段时间内电脑一直在等待VSC的某个东西结束而无法关机.

  • 其余其实还蛮不错的(), 插件同步也很方便, 不过由于VSC的vim模式真的是太精简的一个vim子集了, 不想用阉割的vim, 索性就直接跑去完全的vim了(想起来转移阵地的原因了)

使用环境

最初是用的原生vim + vim script什么的, 后来转去了neovim, 并且企图使用纯lua管理配置文件和各类插件, 由于这块的折腾实在过于庞杂, 而且貌似需要理解vim-lsp等相关知识, 以及各类其余相关知识, 折腾程度不亚于目前折腾的bspwm… 因而目前使用的是低配版的neovim以供学习.

缓冲区、窗口与选项卡

其实关于这一块貌似还蛮重要, 但是一直依仗着bspwm的高效分屏所以也没去搞懂, 导致一些插件关于tabs的配置也不太懂.

缓冲区Buffer

buffer本质是内存中的一块空间, 我们在这里编辑文本; 在vim中每打开一个文件, 该文件就与一个buffer绑定.

通过:buffers, :ls:files查看所有的buff.

通过:bnext, bprevious在buffer之间跳转, 或是通过:buffer + {文件名, n} 跳转到指定buff

通过C-OC-I可以跳转到旧的位置, 不过这不是buffer的特有方法, C-^可以跳转到先前编辑过的buffer.

通过:bdelete + {文件名, n}可以可以删除buffer.

由于buffer的存在, 可以通过-all来同时退出或保存所有的buffer; 如:qall关闭所有的buffer, :qall!关闭所有buffer且不保存, :wqall保存所有buffer并退出.

这样就不用冒着按到q:的风险疯狂按:q了…可喜可贺…

窗口Windows

窗口的概念比较熟悉, buffer们正是借助窗口进行展示的.

通过:split [filename]C-W S创建水平窗口, 通过:vsplit [filename]C-W V创建垂直窗口; 好像:new:split作用一样, 都会打开一个水平窗口.

通过:quitC-W C关闭当前窗口; 注意, 关闭窗口后buffer仍然不会消失.

通过C-W O关闭当前窗口外的所有其他窗口.

通过C-W {h,j,k,l}在窗口之间移动, 通过C-W {H,J,K,L}移动窗口.

真不错, 但是不在工位上屏幕太小了, 可能最多3个窗口顶天了

选项卡Tabs

Tabs是窗口的集合, 将窗口想象为一个XoY坐标系, Tabs则是为其添加了Z轴. 同样的, 在vim中关闭一个tab仅仅是关闭了相应的布局, 而不是关闭一个文件, 文件存储在buffer中.

github-books中提到:

在windows之间移动就像在笛卡尔坐标系的二维平面上沿着X-Y轴移动。您可以使用Ctrl-W H/J/K/L移动到上面、右侧、下面、以及左侧的窗口。

在buffer之间移动就像在笛卡尔坐标系的Z轴上穿梭。想象您的buffer文件在Z轴上呈线性排列,您可以使用:bnext和bprevious在Z轴上一次一个buffer地遍历。您也可以使用:buffer 文件名/buffer编号在Z轴上跳转到任意坐标。

个人觉得这个描述好像不是很贴切, 因为窗口仅仅是展示buffer用, 每个窗口都可以填入任意的buffer, 那么这样的描述就稍微感觉有点怪, 因而改成了目前本博客的描述.

通过:tabnew [filename]创建一个新tab并打开文件filename, 通过:tabclose关闭tab.

通过:tab{next,previous,first,next}在tab之间跳转.

通过gt可以跳转到下一个tab, gT跳转到上一个tab, {n}gt跳转到第n个tab(当然是从1开始标号的), 这个是很方便的.

文件搜索

find与edit

目前在一个打开的vim编辑器中再检索一个文件只会用:edit再去搜索… 十分的麻烦

事实上检索文件有两个命令findedit, 而find会根据path配置的路径查找文件, 而edit只会在打开文件的当前目录下查找.

可以通过set path?查看当前path, 它是这样的:

1  path=.,/usr/include,,

连续的两个逗号好像表示的是当前目录, 没有细究, 可能同时兼具分隔作用也具备表示当前目录的作用.

通过set path+=......可以添加path选项, 这样在使用find时tab补全会比较方便. 一个例子是在配置文件中写入:

1:set path+=$PWD/***

这样会将当前shell的工作目录下的整个项目都包括进去. 需要注意区分$PWD和当前文件的目录, edit所在的目录是当前文件的目录.

grep

vim有内置的grep, :vimgrep, 缩写为:vim, 语法为:

1:vim /pattern/ file

vim内置的grep使用quickfix进行处理(好像也是个比较高级的可折腾模块, 留坑), 通过:copen可以打开quickfix窗口, 通过:cclose关闭窗口, 通过:c{next,previous,older,newer}在错误列表之间跳转.

vim在使用内置的grep时会将所有结果读入内存, 消耗较多的内存… 因而也可以使用外置grep, 通过语法:grep ....来使用. :grep被拓展的外部程序在变量grepprg中, 默认的是:

1  grepprg=grep -n $* /dev/null

netrw

netrw是内置的文件浏览器, 也是大坑, 遇到过插件对此的优化, 当时颇为震惊.

要运行netrw只需要向vim传递一个目录参数即可, 或者通过:Explore, :{S,V}explore创建一个netrw.

具体使用留坑, 可能要配合插件一起进行说明.

操作符

过滤操作符

动作

文本对象

文本对象可以被 操作符 operations 使用,这里有两类文本对象:

1i + object  内部文本对象
2a + object  外部文本对象

内部文本对象选中的部分不包含包围文本对象的空白或括号等,外部文本对象则包括了包围内容的空白或括号等对象。外部对象总是比内部对象选中的内容更多。

下面是一些常用的文本对象:

 1w		一个单词
 2p		一个段落
 3s		一个句子
 4(或)		一对()
 5{或}		一对{}
 6[或]		一对[]
 7<或>		一对<>
 8t		XML标签
 9"		一对""
10'		一对''
11`		一对``

之前居然一直以为daw是delete a word… 每次都在苦恼要手动补去删掉的空白… 太傻逼了

移动

移动! 高速移动是最吸引我的地方, 能够快速移动决定了一个编辑器是否具有力量和美感!

字符导航

hjkl是字符导航的移动, 每次移动1个字符.

单词导航

还有一些针对单词级别的移动:

  • {w,W} 移动到下一个{单词, 词组}的开头

  • {e,E} 移动到下一个{单词, 词组}的结尾

  • {b,B} 移动到前一个{单词, 词组}的开头

  • g{e,E} 移动到前一个{单词, 词组}的结尾

实际上, 单词词组都是翻译出来的, 在vim中其实是wordWORD, 其区别在于(:h word以及:h WORD):

  					*word*

A word consists of a sequence of letters, digits and underscores, or a sequence of other non-blank characters, separated with white space (spaces, tabs, ). This can be changed with the ‘iskeyword’ option. An empty line is also considered to be a word.

  					*WORD*

A WORD consists of a sequence of non-blank characters, separated with white space. An empty line is also considered to be a WORD.

简单说来, word识别[a-zA-Z0-9_]*或其余的非空字符序列, 而WORD仅识别任意的非空字符序列; 以下面的字符串为例, 当光标起始位于行开头时, W会直接跳转到下一行, 而w会依次跳转到1!最后再跳转到下一行.

1!@#12!@#

行导航

在一行中移动通常有以下位置:

  • {0,$} 本行第一个以及最后一个字符

  • {^,g_} 本行第一个以及最后一个非空字符

  • n| 本行第n列; 以前都是通过0nl实现的qaq

还有一些行内字符搜索的操作符:

  • {f,F} 在同一行向{后,前}搜索第一个匹配

  • {t,T} 在同一行向{后,前}搜索第一个匹配, 并停在匹配前

  • ; 在同一行重复一次搜索

  • , 在同一行的相反方向重复一次搜索

句子与段落导航

如何定义句子和段落?(:h sentence以及:h paragraph):

  					*sentence*

A sentence is defined as ending at a ‘.’, ‘!’ or ‘?’ followed by either the end of a line, or by a space or tab. Any number of closing ‘)’, ‘]’, ‘"’ and ’’’ characters may appear after the ‘.’, ‘!’ or ‘?’ before the spaces, tabs or end of line. A paragraph and section boundary is also a sentence boundary. If the ‘J’ flag is present in ‘cpoptions’, at least two spaces have to follow the punctuation mark; s are not recognized as white space. The definition of a sentence cannot be changed.

  					*paragraph*

A paragraph begins after each empty line, and also at each of a set of paragraph macros, specified by the pairs of characters in the ‘paragraphs’ option. The default is “IPLPPPQPP TPHPLIPpLpItpplpipbp”, which corresponds to the macros “.IP”, “.LP”, etc. (These are nroff macros, so the dot must be in the first column). A section boundary is also a paragraph boundary. Note that a blank line (only containing white space) is NOT a paragraph boundary. Note: this does not include a ‘{’ or ‘}’ in the first column.

甚至还有段落宏什么的, 暂时不予考究; 需要知道的是: 句子以{.!?}和一个{ \n\t}结尾, 段落的开头和结尾都有一个空行, 在大部分情况下是正确的.

通过()可以在句子之间跳转; 通过{}可以在段落之间跳转.

匹配导航

如果在一对括号中间, 可以通过%跳到其中一个上, 十分的方便; 也有类似的插件可以补充%的功能, 暂且留坑.

行号导航

熟悉的gg, G以及ng就不多说了, 还有一个n%可以跳转到文件的n%的位置, 稍微有点用.

窗格导航

通过{H,M,L}跳到当前屏幕的{顶部,中间,底部}, 这个会受到scrolloff参数的影响.

通过n{H,L}跳转到距离{顶部,底部}n行的位置, 感觉这个功能比较鸡肋, 屏幕又不是很大, 而且也不知道相对位置, 不如通过n{j,k}+相对行号的选项好用.

滚动

  • C-{f,b} 向{下,上}滚动一屏

  • C-{d,u} 向{下,上}滚动半屏

  • C-{e,y} 向{下,上}滚动一行; 虽然几乎没有用到过这个规格的滚动, 但是好像启发了我如何修改alacritty的滚动快捷键, 之前是邪教自己随便设置的快捷键来滚动一行, 现在有据可依了 sad, 不能够, 忘记了shell自带的移动规则已经把这些都占用了

  • z{t,z,t}将当前行置于屏幕{顶部,中间,底部}

搜索导航

通过/?可以向后和向前搜索, 通过nN重复上一次搜索或反向重复搜索.

需要学习的是*搜索方式, *#是一对搜索光标下文本的操作, 但是要注意的是, 他们进行的是整词搜索, 等价于/\<pattern\>; 而为了查找那些包含当前单词的字符串则要通过g*以及g#.

位置标记

通过mx标记一个位置, x可以是[a-zA-Z]; 区别在于小写字母是buffer内部的局部标签, 而大写字母是全局标签.

通过`x 精确跳转到x标签的行和列, 而'x仅会跳转到x标签的行.

通过:marks查看所有标签.

跳转

并不是所有的移动都是跳转, 通常任何超过单词导航和当前行导航的移动都可能是一个跳转.

通过:jumps查看跳转列表, 更多可以查询:h jump-motions

通过C-{o,i}可以向{上,下}在跳转之间跳转.

插入模式

使用i, I, a, A, o, O, s, S进入插入模式是比较熟悉的了, 也可以通过gi从当前缓冲区上一次结束插入模式位置开始输入文本, 或者通过gI在当前行第一列的位置开始输入文本.

退出插入模式进入普通模式除了<esc>还可以使用C-[C-c.

居然有一些映射是在输入模式下将esc映射到jj或者jk, 感觉十分的邪教…

1inoremap jj <esc>
2inoremap jk <esc>

在插入模式下, 通过C-{h,w,u}分别删除一个{字符,单词,行}, 这与shell快捷键是一致的, 但是却没有alt-d什么的, emm偶尔还是会习惯性的按出alt-d企图实现dw的功能..

i_CTRL-X子模式

在插入模式下的C-x将进入子模式以执行更多的一些指令, 这些指令大部分都用于关键词补全(参考:h ins-completion):

 1Completion can be done for:
 2
 31. Whole lines						|i_CTRL-X_CTRL-L|
 42. keywords in the current file				|i_CTRL-X_CTRL-N|
 53. keywords in 'dictionary'				|i_CTRL-X_CTRL-K|
 64. keywords in 'thesaurus', thesaurus-style		|i_CTRL-X_CTRL-T|
 75. keywords in the current and included files		|i_CTRL-X_CTRL-I|
 86. tags							|i_CTRL-X_CTRL-]|
 97. file names						|i_CTRL-X_CTRL-F|
108. definitions or macros				|i_CTRL-X_CTRL-D|
119. Vim command-line					|i_CTRL-X_CTRL-V|
1210. User defined completion				|i_CTRL-X_CTRL-U|
1311. omni completion					|i_CTRL-X_CTRL-O|
1412. Spelling suggestions				|i_CTRL-X_s|
1513. keywords in 'complete'				|i_CTRL-N| |i_CTRL-P|

Vim通常会关注所有buffer中的内容来作为补全的来源, 自动补全也是个大坑, 后续可能会专门开坑详细说明与插件的结合.

还有两个用于屏幕滚动:

1CTRL-X CTRL-E		scroll window one line up.
2CTRL-X CTRL-Y		scroll window one line down.

i_CTRL-O子模式

在输入模式下通过<C-o>可以进入到insert-normal子模式, 在此模式下可以执行一条普通模式的命令.

修改

gn

gn是一个移动并选择的动作, 它向后搜索和上一个搜索的模式匹配的位置,并且 自动对匹配的文本进行可视化模式下的选取.

比如, 尝试将下面的let替换为const:

1let one = "1";
2let two = "2";
3let three = "3";

可以使用/letcwconst紧接着n . n ., 更快的方式是/letcgnconst<esc>然后. .即可.

在修改中使用搜索命令

在这样一段文字中:

1a
2b
3foo
4c
5d
6foo
7e
8f

通过d/foo会直接删除a, b, 然后.后vim会删除foo, c, d, 再按一次.后则什么也不会发生因为后面没有foo来匹配了.

寄存器

原文中提到:

学习Vim中的寄存器就像第一次学习线性代数一样,除非你学习了他们,否则你会觉得自己根本不需要它们。

好像确实有点道理喔…

寄存器命令

如何使用寄存器?

可以通过{y,c,d,s,x}等操作向寄存器中存储, 也可以通过{p,P}等从寄存器中取出(put)/粘贴(paste)出文本文本, 在取文本时, 通用语法为"x, x是寄存器的标志.

比如: 在普通模式下通过"xyiw可以指定yiw的目的地为寄存器x, 在插入模式下通过C-r x即可粘贴存放在寄存器x的内容

也可以通过:put x命令获取x寄存器中的内容, 其与p的区别在于p在当前光标后插入内容, 而:put新起一行再打印寄存器内容. 由于这是一个命令行命令, 所以可以传递地址给他. 比如::10put a将在往下第10行处新起一行插入a的内容.

寄存器类型

共有10种寄存器类型(:h registers):

There are ten types of registers: registers {register} E354

  1. The unnamed register ""
  2. 10 numbered registers "0 to "9
  3. The small delete register "-
  4. 26 named registers "a to "z or "A to "Z
  5. Three read-only registers ":, "., "%
  6. Alternate buffer register "#
  7. The expression register "=
  8. The selection registers "* and "+
  9. The black hole register "_
  10. Last search pattern register "/

下面将对这10类寄存器进行说明.

匿名寄存器

匿名寄存器默认存储着你最近一次复制, 修改或删除的文本. 如果再进行另一次复制, 修改或删除, Vim会自动替换匿名寄存器中的文本. 匿名寄存器和电脑上粘贴板的功能很接近.

默认情况下, p(或者P)是和匿名寄存器相关联的, 随后使用p而不是""p来指代匿名寄存器.

编号寄存器

一共有两种不同的编号寄存器:复制寄存器(0)和其他编号寄存器(1-9).

  • 复制寄存器: 如果使用yy复制一行文本, 其会被vim自动存放到匿名寄存器和0号寄存器中(不足一行也是!). 后续除非再次执行复制操作, 不然均可以通过0号寄存器获得此前复制的内容.

  • 其他编号寄存器: 当修改或者删除至少一整行的文本时, 这部分文本会按照时间顺序存放在1-9号寄存器中. 比如:

1line three
2line two
3line one

逐行通过dd删除, 可以通过:regsiters看到1,2,3号寄存器分别对应了one, two, three.

此外, .命令默认提供了便利: 如果在上述情况下, 第一次使用"1p会粘贴1号寄存器的内容, 再次使用.后会自动粘贴2号寄存器的内容, 以此类推.

有一点像入栈、弹栈, 但是并不完全一样, 因为点命令不会实际修改1-9号寄存器的顺序, 只是好像是默认递增上一次"1p的序号罢了.

小删除寄存器

不足一行的修改和删除不会被存放在任何编号寄存器中, 他们被存放在小删除寄存器("-)中. 不过小删除寄存器只能存储一个文本, 在一些情况下为了保存更多的小文本可以使用命名寄存器.

命名寄存器

a-z命名寄存器可以存储复制、修改和被删除的文本.

有时你可能会想要往已有内容的命名寄存器中继续添加内容,这种情况下,你可以追加文本而不是全部重来。你可以使用大写版本的命名寄存器来进行文本的追加。

只读寄存器

vim仅有3个只读寄存器:

1.    存储上一个输入的文本
2:    存储上一次执行的命令
3%    存储当前文件的文件名

交替Buffer寄存器

大部分情况下, 这个寄存器是当前窗口中上一次访问的缓冲区.

可以通过"#p输出上一次访问的buffer的文件名.

不知道有啥用.. 感觉没啥用..

表达式寄存器

表达式寄存器"=用于计算表达式的结果.

通过"=1+1<Enter>p可以将表达式输出. 在这个地方, p输出的是表达式寄存器的内容, 不过如果通过命令:registers查看寄存器却不能看到匿名寄存器中存放着表达式的结果, 所以料想这里的p是一个与表达式计算绑定的特殊输出了. 事实上, 第二次使用p才会输出匿名寄存器中的内容.

与其余寄存器一样, 在插入模式中通过Ctrl+r =可以计算表达式.

原文还说, 还可以通过@来将其他寄存器中的内容作为表达式的输入, 比如从寄存器a中获取文本并计算:

1"=@a

事实上好像不行. @其实是执行寄存器中的内容若干次, 录制宏也是一样. 比如此时寄存器a中为1+2, 执行"=@a<Enter>p仅仅会再次输出一次1+2, 不知道为啥. chatgpt告诉我可以通过函数eval(@a)来计算, 确实有道理.

选取寄存器

Vim有两个选取寄存器: quotestar ("*) 和 quoteplus ("+). 可以用它们来访问从外部程序中复制的文本。

这两个寄存器的区别在于:

你也许会想如果"和"+能办到的事完全相同,那为什么Vim需要两个不同的寄存器呢?一些机器使用的是X11窗口系统,这一系统有3个类型的选项:首选,次选和粘贴板。如果你的机器使用的是X11的话,Vim使用的是quotestar (")寄存器作为X11的首选选项,并使用 quoteplus ("+)作为粘贴板选项。这只在你的Vim配置里开启了xterm_clipboard 选项时才有效(vim –version中的+xterm_clipboard)。如果你的的Vim配置中没有 xterm_clipboard也不是什么大问题。这只是意味着quotestar 和quoteplus两个寄存器是可以互相替代的。

可以通过设置set clipboard=unnamed, 这样可以直接通过p来put进外界的内容.

黑洞寄存器

黑洞寄存器("_)类似于/dev/null, 用于不想要保存到寄存器中的删除和修改文本操作.

一个很酷的技巧是将黑洞寄存器("_)传给:put命令. 因为黑洞寄存器不保存任何值, :put _命令将插入一个新的空白行. 您可将这个与全局命令联合起来, 插入多个空行. 比如, 要在所有以文本"end"结尾的行下插入空行, 使用:g/end/put _

搜索模式寄存器

搜索模式寄存器("/)存放了上一个搜索的条目.

插件与工具

fzf与ripgrep

参考资料

github Learn-Vim_zh_cn

Vim中的tabs

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核, 后端开发, Python, Rust 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改记录:
  • 2023-05-29 23:05:14大幅重构了python脚本的目录结构,实现了若干操作博客内容、sqlite的助手函数;修改原本的文本数 据库(ok)为sqlite数据库,通过嵌入front-matter的page_id将源文件与网页文件相关联
  • 2023-05-08 21:44:36博客架构修改升级
  • 2022-12-31 20:34:48更新了vim使用技巧之寄存器等
  • 2022-12-19 13:08:52更新了插入模式的两个子模式
  • 2022-12-13 15:16:11vim使用技巧Ch1-5
vim使用技巧