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

阅读数:1874 2019 年 10 月 21 日 17:08

一篇文章吸取 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 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
{1}
" local project headers
setlocal path=.,,*/include/**3,./*/include/**3
" basic system headers
setlocal path+=/usr/include
{1}
setlocal tags=./tags,tags;~
" ^ in working dir, or parents
" ^ sibling of open file
{1}
" 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

评论

发布