一篇文章吸取 Vim 全部精华(下)

2019 年 10 月 21 日

一篇文章吸取 Vim 全部精华(下)

本文翻译自“History and effective use of Vim”,翻译已获得原作者 Joe Nelson 授权。


本文是“Vim发展历史及高级用法(上)”的续篇。


包含与 path


许多编程语言都允许你在一个模块或文件中,包含另一个模块或文件。有了pathincludesuffixesaddincludeexpr等设置项,Vim 就会知道如何在包含的文件中搜索程序标志符。用 ctag 可以维护一个标签文件,相似的功能用标志符搜索(帮助见:help include-search)也能完成。


这些设置项天生支持 C 语言,也支持其它语言,但有可能需要调整。这些不在本文的讨论范围之内了,请查找帮助:help include


所有东西都配置好之后,在某个标志符上输入[i就可以显示它的定义,也可以输入[d来显示宏定义。当你在一个文件名上输入gf时,Vim 会在 path 中找到这个文件,并直接跳转过去。因为 path 的内容也会影响:find命令的结果,所以有的人喜欢把“**/*”或经常访问的目录都加到 path 里来,这样就可以把:find当成一个模糊查找器了。不过,这么做会搜索与当前任务不相干的目录,因此会让搜索标志符的操作变慢。


如果觉得这样的搜索功能勉强可以使用,而又不想污染 path 的内容,就得再做一个映射了。你可以敲下(一般是反斜杠+空格),然后输入文件名,再用 tab 或 CTRL-D 完成功能来找到文件。


" fuzzy-find litenmap <Leader><space> :e ./**/
复制代码


再强调一遍:path 参数是为头文件设计的。你甚至可以试试:checkpath命令,来看看 path 是不是工作正常。打开一个 C 文件并运行:checkpath,它会把所有当前文件包含了但又找不着的文件显示出来。再加上一个叹号(:checkpath!)会显示当前文件包含的所有文件的完整层次架构。


path 默认会包含“.,/usr/include,”,这表示当前目录、/usr/include 和活跃缓冲区的所有兄弟文件。目录描述符和 glob 的功能也都很强大,可以由:help file-searching来了解更多细节。


在我的 C ftplugin 项目里(后面会细讲),我把路径搜索功能设置成了针对当前项目内的文件,比如./src/include 或./include。


setlocal path=.,,*/include/**3,./*/include/**3setlocal path+=/usr/include
复制代码


两个星号加上个数字(比如**3)表示对子目录的搜索深度。为搜索深度加上限制是个好习惯,这样可以避免搜索标志符时锁死。


如果:checkpath显示某些文件在你的项目里面找不着,那可以考虑在你的 path 里面增加更多的模式。当然这与你的系统有关。


  • 更多系统路径:/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快捷键nmap ]q :cnext<cr>nmap ]Q :clast<cr>nmap [q :cprev<cr>nmap [Q :cfirst<cr>
复制代码


如果修改了程序又再次构建之后,你还想看看上次的出错信息,这时候可以使用:colder(然后用:cnewer返回)。用:cc可以查看更多关于当前选中的错误的信息,用:copen可以查看完整的 quickfix 缓冲区。如果不运行:make,则可以使用:cfile:caddfile:cexpr自己操作 quickfix 的内容。


Vim 根据出错消息的格式来解析构建过程的输出内容,其中可以使用类似 scanf 的转义序列。一般来说都会把这类内容放到一个“编译器文件”中。比如,Vim 自带了一个 gcc 的编译器文件 $VIMRUNTIME/compiler/gcc.vim,但没有针对 clang 的。下面是我创建的 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 的“compiler”:


CompilerSet errorformat=%f:%l:\ %mCompilerSet makeprg=diction\ -s\ %
复制代码


运行:compiler diction,然后就可以正常使用:make命令来运行它,并生成 quickfix 缓冲区的内容了。我的.vimrc 里还有个小亮点,就是对运行 make 的映射:


" real makemap <silent> <F5> :make<cr><cr><cr>" GNUism, for building recursivelymap <silent> <s-F5> :make -w<cr><cr><cr>
复制代码


对比与补丁


Vim 自带的对比(diff)功能很强大,但也很难驾驭,尤其是三向合并视图。而事实上如果你肯花时间去研究的话,也不是那么难。它最主要的理念是每个窗口或者处于、或者不处于对比模式下。所有进入对比模式(:difft[his])的窗口都会与所有其它已经处于对比模式下的窗口进行比较。


为简单起见,我们以两个文件为例:


echo "hello, world" > h1echo "goodbye, world" > h2
vim h1 h2
复制代码


在 Vim 里,用:all命令把所有文件分别显示出来。在顶部 h1 的窗口里运行:difft,然后连按两次 CTWL-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。请记住这些命令只是把文件加载到 Vim 窗口里,并设置对比模式而已。


有了这些做铺垫,接下来我们再学习如何把 Vim 用作 git 的三向合并工具。先配置 git:


git config merge.tool vimdiffgit config merge.conflictstyle diff3git config mergetool.prompt false
复制代码


当你碰上合并冲突时,就运行git mergetool。它会运行 Vim,打开四个窗口。这一块还是比较复杂的,我也经常是乱试一阵,最终无功而返。


+-----------+------------+------------+|           |            |            ||           |            |            ||   LOCAL   |    BASE    |   REMOTE   |+-----------+------------+------------+|                                     ||                                     ||             (edit me)               |+-------------------------------------+
复制代码


有个小窍门:只在底下的窗口里进行编辑。上面的三个窗口只用来显示文件在合并时本地与远端(local 与 remote)两者之间的差异,以及它们的共同基础版本(base)。


]c在下面的窗口里移动,对每个有差异的块,选择使用 local、base 或 remote 的哪一个,或者自己写。


我在我的.vimrc 里设置了一些映射,可以更容易地从上面的窗口里把修改内容拉下来:


" shortcuts for 3-way mergemap <Leader>1 :diffget LOCAL<CR>map <Leader>2 :diffget BASE<CR>map <Leader>3 :diffget REMOTE<CR>
复制代码


我们已经知道了:diffget的用法,这里就是用不同缓冲区的名字标志不同的窗口,再做为参数传给:diffget,绑定起来。


合并结束后,运行:wqa保存退出。如果又不想保留这些修改内容了,就运行:cq,这样会给 shell 返回一个错误码,并给 git 发信号,让它忽略你的修改。


Diffget 也可以按范围进行处理。如果想接受某个窗口的全部改动,而不是一块接一块的拉取,可以直接运行:1,$+1diffget {LOCAL,BASE,REMOTE}。这里“+1”是必要的,因为在缓冲区最后一行的“下面”,也可能还会有已删除的行。


三向合并还是挺简单的,所以用不上 Fugitive 之类的插件,至少做简单的展示解决合并冲突时是这样。


补丁 8.1.0360 让 Vim 捆绑了 xdiff 库,可以在内部直接进行对比。这样就比让外部程序进行对比高效得多,而且也支持更换对比算法。“patience”算法产生的结果比默认的“myers”算法更易读,可以用如下方法在.vimrc 里面进行设置:


if has("patch-8.1.0360")  set diffopt+=internal,algorithm:patienceendif
复制代码


Buffer I/O


下面这个场景是否似曾相似?你进行修改之后,想把它保存成一个新文件,于是你执行了:w newname。又改了一些东西之后,你执行了:w,但它却修改了最早的文件。事实上在这个场景下你希望的是:save as 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  编辑foot文件,即使当前缓存区有修改也不显示
复制代码


在上面用到tr命令的例子里,我们进行了 ROT-13 加密,实际上 Vim 已经用g?命令内置了这个功能,用g?$命令就可以达到相同目的。


文件类型


文件类型(Filetype)是一种根据打开文件的类型来改变设置的方法。这不需要自动检测,我们可以手动启用这些有趣的效果。编辑 16 进制文件就是个例子。所有文件都可以看做是 16 进制编码的。GitHub 用户 the9ball开发了一个很棒的 ftplugin 脚本,可以用进行 16 进制编辑的 xxd 工具对缓冲区进行反复过滤。


为了方便,xxd 被打包成了 Vim 5 的一部分。Vim 的 todo.txt 里提到,他们想让它可以更顺畅地编辑二进制文件,但事实上 xxd 能做的远不止这些。


你可以把下面的代码放到~/.vim/ftplugin/xxd.vim里。配置在 ftplugin 里意味着当文件类型(FileType,大家简称 ft)变成 xxd 时,Vim 就会执行这个脚本。我向脚本里加了些简单注释。


" without the xxd command this is all pointlessif !executable('xxd')  finishendif
" don't insert a newline in the final line if it" doesn't already exist, and don't insert linebreakssetlocal binary noendoflinesilent %!xxd -g 1%s/\r$//e
" put the autocmds into a group for easy removal lateraugroup 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_cursoraugroup 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 valuelet b:undo_ftplugin = 'setl bin< eol< | execute "au! ftplugin-xxd * <buffer>" | execute "silent %!xxd -r"'
复制代码


试着打开一个文件,并运行:set ft,留意一下文件类型。再运行:set ft=xxd,Vim 就会变成一个 16 进制编辑器。要恢复成原来的视图的话,假如原来的文件类型是 foo,就运行:set ft=foo。注意在 16 进制视图中语法也是高亮的,因为$VIMRUNTIME/syntax/xxd.vim是 Vim 自带的。


注意一下“b:undo_ftplugin”的用法,当用户或 ftdetect 机制切换了文件类型时,这是一个文件类型自我清理的好时机。上面的例子里费了些力气,因为当你:set ft=xxd再设置回来时,即使你没有进行任何修改,缓冲区仍会被标记为有改动的。


Ftplugin 也允许你重定义一个已有的文件类型。比如,在$VIMRUNTIME/ftplugin/c.vim里,Vim 已经针对 C 语言有了很多不错的默认设置。我则在我的~/.vim/after/ftplugin/c.vim顶部增加了一些自己的设置:


" the smartest indent engine for Csetlocal cindent" my preferred "Allman" style indentationsetlocal cino="Ls,:0,l1,t0,(s,U1,W4"
" for quickfix errorformatcompiler clang" shows long build messages bettersetlocal ch=2
" auto-create folds per grammarsetlocal foldmethod=syntaxsetlocal foldlevel=10
" local project headerssetlocal path=.,,*/include/**3,./*/include/**3" basic system headerssetlocal 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 annoyingsetlocal completeopt=menu
iabbrev #i #includeiabbrev #d #defineiabbrev main() int main(int argc, char **argv)
" add #include guardiabbrev #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
复制代码


用英文句号也可以混合文件类型。比如不同的项目有不同的编码规范,所以你可以把默认的 C 语言设置与某个项目的独特设置结合起来。OpenBSD 源码遵循style(9)格式,那我们就可以生成一种独特的 openbsd 文件类型。再用:set ft=c.openbsd把两种文件类型结合起来。


要检测 openbsd 文件类型,我们也可以查看缓冲区里的内容,而不仅仅是看文件扩展名和在磁盘上的位置。OpenBSD源码中的 C 文件第一行都包含“/* $OpenBSD:”,这就是个很好的标记。


要检测它们,创建~/.vim/after/ftdetect/openbsd.vim:


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


移植到OpenBSD的Vim已经为这种文件类型包含了一个单独的语法文件:/usr/local/share/vim/vimfiles/syntax/openbsd.vim。如果你还记得,/usr/local/share/vim/vimfiles目录是在运行时路径里的,是给系统管理员存放文件的。这个 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=80endfun
复制代码


我们只需要在合适的时机调用这个函数就好了。创建~/.vim/after/ftplugin/openbsd.vim


call OpenBSD_Style()
复制代码


现在再打开 C 文件或头文件,如果顶部有那串独特的注释,就会被认为是 c.openbsd 类型,因此就会使用符合 style(9)规范的缩进设置。


不要忘了鼠标


友情提示一下,尽管我们已经把命令行玩得很顺了,但也不要忘了 Vim 也是支持鼠标的,有时候比键盘还方便。由于有 Xterm 可以把鼠标事件转换成标准输入转义码,我们甚至可以通过 SSH 发送鼠标事件。


要启用鼠标,只需设置mouse=n。很多人喜欢mouse=a,让鼠标在所有模式下都可用,但我还是喜欢在正常模式下才启用它。这样我用键盘修改器点击一个链接,想在浏览器中打开它的时候,才不会造成选中的效果。


鼠标能做的事有:


  • 打开或关闭折叠效果(当折叠行数大于0时)

  • 选择标签(敲gt gt gt……)

  • 点击来结束一个动作,像d<click!>一样。这个与easymotion插件很相似,但不需要安装插件

  • 通过双击跳转到帮助

  • 拖动底部的状态行,改变命令窗口的高度

  • 拖动窗口边缘来改变窗口的大小

  • 滚轮


各种编辑


这一节的内容可以无穷无尽,但我会主要讲解我学到的一些技巧。首先是:set virtualedit=all,它让你可以把光标移动到窗口的任何位置。如果你输入一些字符,或者插入一个可视块,Vim 会自动在左边插入足够的空格,来保证它们处于希望的位置。可视化编辑模式在编辑表格数据时很有用。用:set virtualedit=可以关闭它。


接下来是一些移动命令。我以前总是用“}”在段落之间跳跃,或者干脆不断地按翻页键。事实上“]”字符可以让动作变得更精准:按函数]]、范围]}、括号])、注释]/、对比块]c等。了解这些之后,我们就知道为什么前面提到的 quickfix 映射]q可以对模式适配得这么好。


大范围的跳跃,我喜欢用1000j之类的命令。在正常模式下,也可以直接输入像50%这样的百分比,Vim 就会直接跳转过去。说到页面显示的百分比,你也可以随时用 Ctrl-G 查看。我喜欢用:set noruler,等需要看这些信息的时候再查看,这样界面上就没那么杂乱。许多人喜欢 Powerline 之类提供的五颜六色的风格,这方面我有些不合潮流。


当你在标签、文件之间或文件内部跳跃时,也有些命令可以帮你找到自己的位置,如:ls:tags:jumps:marks。在标签之间的跳跃动作会产生一个堆栈,可以用 Ctrl-T 命令出栈一个动作。我常用 Ctrl-O 从跳跃中退出,但这没有出栈动作那么直接。


在一个用 ctag 索引过的项目目录下,可以用-t直接带着标签名打开编辑器,比如vim -t main。要更灵活地找到标签文件,可以设置tags配置变量。请注意下面例子中的分号,它让 Vim 从当前目录开始,一直搜索到 HOME 目录。这样在项目目录之外,你就有了更通用的系统标签文件。


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,等等。


你会不会先拷贝了一个单词(yw),在别处又删了另一个单词,接下来试图粘贴前面的单词,结果发现它已经被后面删除的内容覆盖了?在这一点上,Vim 的寄存器的确令人失望。你可以用:reg命令查看它们的内容。当你拷贝文本之后,之前拷贝的内容就会被切换到寄存器"0"9里面去了。所以"0p会粘贴上一次拷贝或删除的内容。另外还有寄存器命令"+"*可以针对系统剪贴板进行操作。一般来说它们是同一回事,除了在某些 X11 设置中,它们会区分第一和第二选择。


命令行窗口也要提一下。这是个缓冲区,保存着你之前执行过的命令或搜索。你可以用q:q/打开它,然后移动到任意一行,直接回车执行。你也可以在回车之前先对它进行编辑。你的修改不会影响当前行内容,新的命令会被追加到列表底部。


这篇文章还可以写很多内容,但我准备到此为止了。对更多内容感兴趣的话,读者可以自己查看帮助:views-sessions、viminfo、TOhtml、ins-completion、cmdline-completion、multi-repeat、scroll-cursor、text-objects、grep、netrw-contents。


原文链接


History and effective use of Vim


2019 年 10 月 21 日 17:082535

评论

发布
暂无评论
发现更多内容

架构师训练营 No.3 周总结

连增申

架构师训练营第三周作业

hiqian

组合设计模式-打印窗口组件的树状结构

张磊

第三周作业一

carol

单例模式 组合模式

作业-02

梦子说

极客大学架构师训练营 作业

作业03-代码重构

梦子说

极客大学架构师训练营 命题作业

代码重构

dongge

homework 3

东哥

架构师训练营 - 学习总结 - 第三讲

吕浩

如何有效实现“科技抗疫”?这家科技巨头正在作出典范

飞天鱼2017

第三章作业

武鹏

助力经济复苏 联想来酷"618"聚"惠"来袭

Geek_116789

新来的"大神"用策略模式把if else给"优化"了,技术总监说:能不能想好了再改?

Hollis

Java 设计模式

总结-02-设计模式

梦子说

学习 极客大学架构师训练营

如果你想要说服别人,要诉诸利益,而非诉诸理性

Neco.W

理性 说服 利益 谈判

单例模式的实现方式

互金从业者X

架构训练营 0 期总结 -- 第三周

互金从业者X

架构师训练营第 3 周作业

Season

单例模式 极客大学架构师训练营 组合模式

只看到了别人28岁从字节跳动退休,背后的期权知识你知道吗?

四猿外

创业 程序员 字节跳动 个人成长 期权

【架构师训练营】第三期

云064

Git | Git 操作整理-基础篇

多选参数

git GitHub 版本控制 版本管理工具

ARTS-Week 02

chasel

架构师训练营第三周作业

sunnywhy

架构师训练营第三周总结

邵帅

可读代码编写炸鸡一

多选参数

代码 代码组织 代码规范

架构师训练营第三周作业

James-Pang

极客大学架构师训练营

设计模式的应用

carol

总结

你不知道的 Web Workers (上)

阿宝哥

Java Web 前端开发 Web Worker

架构师训练营第三周总结

James-Pang

极客大学架构师训练营

架构师训练营第三周总结

sunnywhy

极客时间架构师训练营 - week3 - 作业 1

jjn0703

极客时间 极客大学架构师训练营

一篇文章吸取 Vim 全部精华(下)-InfoQ