首页
学习
活动
专区
工具
TVP
发布

一篇文章吸取 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 lite
nmap <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/**3
setlocal 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:\ %m
CompilerSet makeprg=diction\ -s\ %

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

" real make
map <silent> <F5> :make<cr><cr><cr>
" GNUism, for building recursively
map <silent> <s-F5> :make -w<cr><cr><cr>

对比与补丁

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

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

echo "hello, world" > h1
echo "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 vimdiff
git config merge.conflictstyle diff3
git config mergetool.prompt false

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

+-----------+------------+------------+
|           |            |            |
|           |            |            |
|   LOCAL   |    BASE    |   REMOTE   |
+-----------+------------+------------+
|                                     |
|                                     |
|             (edit me)               |
+-------------------------------------+

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

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

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

" shortcuts for 3-way merge
map <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:patience
endif

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 pointless
if !executable('xxd')
	finish
endif

" don't insert a newline in the final line if it
" doesn't already exist, and don't insert linebreaks
setlocal binary noendofline
silent %!xxd -g 1
%s/\r$//e

" put the autocmds into a group for easy removal later
augroup 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_cursor
augroup 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 value
let 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 C
setlocal cindent
" my preferred "Allman" style indentation
setlocal cino="Ls,:0,l1,t0,(s,U1,W4"

" for quickfix errorformat
compiler clang
" shows long build messages better
setlocal ch=2

" auto-create folds per grammar
setlocal foldmethod=syntax
setlocal foldlevel=10

" local project headers
setlocal path=.,,*/include/**3,./*/include/**3
" basic system headers
setlocal 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 annoying
setlocal completeopt=menu

iabbrev #i #include
iabbrev #d #define
iabbrev main() int main(int argc, char **argv)

" add #include guard
iabbrev #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
                \| endif
augroup 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=80
endfun

我们只需要在合适的时机调用这个函数就好了。创建~/.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”,翻译已获得原作者Joe Nelson授权。

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

原文链接History and effective use of Vim

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/4ghjw0zc5T67Hyyik93l
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券