vim 最佳实践


原文链接: vim 最佳实践

简单的那些「set xxx」之类的命令就不提了,先从 map 开始吧。
map 命令的用途是把一组键映射为其他的命令。
例如我想在按「;」键的时候,自动变成「:」键进入命令行模式,就可以这样设置:

map ; :

Vim 的搜索模式太古怪,我比较习惯 Python 的风格,所以可以把「/」替换成「/\v」,变成 very magic 模式:

map / /\v

还有设置了 hlsearch 后,搜索结果就一直高亮了,切换它又比较麻烦。还好我习惯没事多按几次 Esc 键,所以可以在按 Esc 键时清空搜索寄存器,这样就不会再高亮搜索结果了:

map :let @/=""

这样设置后,虽然效果达到了,可是引入了一个副作用:方向键的行为变得很诡异了。搜了下原因,好像是方向键也会触发 Esc 键,而且好像没法解决。于是只好换个快捷键了:

let mapleader=","
map / :let @/=""

mapleader 是自定义命令的起始键,一般都定义成逗号。因为「/」离「,」比较近,而且表示搜索的意思,所以我就把快捷键设为「,/」了。如果为了方便的话,「,,」或「,」会更快。

你会发现这个设置在普通模式和可视模式下都可用,但是插入模式下并不会生效。
如果要针对某个模式的话,需要加上这个模式的前缀,常用的有 nmap、vmap、imap 和 cmap 等,分别适用于普通、可视、插入和命令行模式。
例如我在插入模式的时候,想回到上一个单词,一般需要按下 Esc 进入普通模式,再按 b 到上一个单词,然后按 i 进入插入模式。于是可以把这些操作绑定到 Ctrl + b 快捷键上:

imap bi

然而你在使用时会遇到问题,按 Esc 键从插入模式返回普通模式时,会让光标左移一位,再按 b 有可能就定位错误了。这种情况下,就可以用 Ctrl + o 来临时切换到普通模式,在执行完 b 命令后,会自动回到插入模式:

imap b

如果想让 Esc 键不让光标左移,可以这样设置:

imap :stopinsert

另外,回退一个单词用这个快捷键也是有效的:

imap

不过我直接按 Shift + ← 并没反应,原因是大部分终端模拟器并不能区分 Shift 和不可见字符的组合。另外,CMD 键也是不可用的。如果非要使用这二者的话,只能用 MacVim。
由于 MBP 上没有 Home 键,因此这样回到行首也是可行的:

imap

接着又出现了一个问题,假设我把 Home 键设置成回到页首:

imap gg

然后我按 Ctrl + a 时,也会回到页首,而不是行首了,这种不可控的事是程序员不愿意遇到的。
所以要加上 nore 这个前缀,以确保替换成的命令是不会因其他设置而改动的:

inoremap

如果要问什么时候该加 nore,答案是如果你没把握,那就加上。

目前为止,一切看上去都很正常,但是好像又有什么不对:为什么我在普通模式替换的是普通按键,而在插入模式替换的却是快捷键?
前半个问题很好回答:因为没这个必要,在普通模式下,我只需要像普通的命令一样连续按键就行了,为什么要用快捷键这种需要同时按住多个按键的方式?
后半个问题则主要是识别问题:插入模式下,按下按键需要立刻输出到屏幕上,如果要判断是否是一个命令,就得等待一段时间,确认不是命令后才能输出,导致输入体验不佳。
以下面的例子为例,假设我想按 2 下引号,就在当前单词的前后各插入一个引号:

inoremap "" viwvb"ea"

只要快速按 2 下引号,大部分时候确实是能用的;但如果我只是想输入一个引号,就会看到等一秒后光标位置才会右移(虽然并不妨碍我继续输入后面的字符)。
不过,如果只有一个按键,不需要等待匹配下一个,那就不会影响输入体验了。例如自动补全括号和引号:

inoremap ( ()
inoremap [ []
inoremap { {}
inoremap " ""
inoremap ' ''

而在命令行模式下,一般都要敲入回车才真正执行一条命令,但 cmap 会让匹配的命令立刻执行,不需要敲入回车,这也显得比较诡异。
所以大部分情况下,插入模式和命令行模式都只定义一些快捷键的 map。

另外,map 还能加一些特殊参数:

<buffer> 表示只对当前文件有效。
<nowait> 表示不等待范围更大的组合匹配。例如同时定义这两组:

nnoremap <Leader>wd dw
nnoremap <buffer> <nowait> <Leader>w w

如果没有加「<nowait>」,按下「,w」后,要等一段时间确认之后你接下来要按的不是「d」,才会执行。加了之后就不会等待,而是直接执行了,缺点就是「,wd」会失效。
<silent> 表示不在命令行显示输入的命令。

其他不是很常用,就不说了。

说完 map 后,再回到之前未解决的一个问题:命令行模式下,如何自定义命令?
这就该轮到 command 出场了。
以保存文件为例,有时候打开了一些系统文件,编辑完后却发现不能保存,要输入一段很长的代码才能用 sudo 来保存。
现在就用 :W 命令(必须以大写字母开头)来简化这个操作:

command W :w !sudo tee %

无论怎样,它至少是能用的,但是保存时会出现一个确认界面,有点难看,于是这样去掉:

command W :silent w !sudo tee %

可如果设置了检查文件是否改动,保存完后还会提示你文件被修改了。于是可以用 execute 命令来执行,执行完后再用 :e! 来编辑:

command W :execute 'silent w !sudo tee %' | :e!

还有个不爽之处是命令行仍然会闪动一下,输出一片内容然后很快消失掉,可以将输出重定向到 /dev/null 来解决:

command W :execute 'silent w !sudo tee % > /dev/null' | :e!

再把 :WQ 给加上:

command WQ :execute 'silent w !sudo tee % > /dev/null' | :q!

或者偷下懒,在 :W 命令后再执行 :q:

command WQ :execute 'W' | :q

最后,如果设置了自动重新载入 .vimrc 文件,重复加载时会报命令已经定义过的错误。把 command 改成 command! 即可解决。

接着再来说说自动执行的问题。
大部分的编程语言里都用制表符来缩进,而 Python 通常使用 4 个空格。这种情况下,就可以用 autocmd 来解决:

autocmd FileType python set expandtab

当检测到文件类型为 python 时,就会设置 expandtab,把 Tab 展成空格了。
不过再打开其他文件时,仍然是用空格来缩进,于是需要把 set 改成 setlocal,让它只针对当前 buffer 和 window 有效。
另外,和 command 一样,重新加载 .vimrc 文件时,这个事件会再次被绑定,于是会重复执行两次。解决办法是把它加入一个组:

augroup python_indent

autocmd!
autocmd FileType python setlocal expandtab

augroup END

其中,autocmd! 会先清空这个组,而后面的 autocmd 会重新绑定事件。

另一种做法是直接使用 autocmd! 来绑定事件,和 command! 一样,它也会替换之前绑定的事件。
例如这样的设置:

autocmd! BufNewFile *.py call append(0, "test")
autocmd! BufNewFile .py call append(0, "# -- coding: utf-8 -*-")

只有后一句会生效。

顺带一提,自动重新加载 .vimrc 文件也可以用 autocmd 来实现的:

augroup reload_vim_config

autocmd!
autocmd BufWritePost $MYVIMRC source $MYVIMRC

augroup END

再来一个比较复杂点的,保存某些类型的文件时,自动去掉行尾的空格:

augroup strip_traling_spaces

autocmd!
autocmd BufWritePre *.py,*.js,*.css %s/\s\+$//e

augroup END

如果想让读取时也去掉空格,可以把第三行改成

autocmd BufRead,BufWritePre .css,.js,*.py %s/\s+$//e

也就是说,这后面跟的多个事件是「或」的关系。
如果需要「与」的关系,可以用多个 autocmd:

autocmd FileType css,javascript,python autocmd BufWritePre %s/\s+$//e

还能加上 if 来判断:

autocmd BufWritePre * if index(['css', 'javascript', 'python'], &filetype) >= 0 | %s/\s+$//e

其中,&filetype 或 &ft 是当前文件的类型,index 函数可以用来检查是否在列表中。

虽然列出了那么多种方法,但其实都没能解决一个问题:删除空格后,光标位置变了。
在编写 Vimscript 时,不带来副作用也是很重要的一点。而为了解决这个问题,就不得不保存原位置,修改完后再恢复了:

function! StripTrailingSpaces()

let l = line(".")
let c = col(".")
%s/\s\+$//e
call cursor(l, c)

endfunction
augroup strip_traling_spaces

autocmd!
autocmd FileType css,javascript,python autocmd BufWritePre <buffer> call <SID>StripTrailingSpaces()

augroup END

其中,function 后的叹号和 command! 的作用一样,重复定义时覆盖之前的定义。 会被替换成一个唯一的值,避免命名冲突。
另外,如果函数名没有加范围前缀(例如 s: 表示本地函数,g: 表示全局函数),则必须大写。

然后再聊聊复制粘贴的问题。
用 Vim 的人应该都有过把代码粘贴到 Vim 里后,缩进变得乱七八糟的经历。
原因就是直接用 CMD + v 粘贴时,会模拟成用户的输入,而在设置了自动缩进的情况下,缩进就变乱了。
解决起来其实很简单,一种办法是 :set paste 后再粘贴,贴完后再 :set nopaste;另一种办法是直接用 p 来粘贴,但这要求 Vim 能访问系统剪贴板;此外,OS X 上还能调用 pbpaste 这个外部命令来粘贴。
第一种办法因为我没法在 MBP 上绑定 CMD + v 快捷键,所以就不管了。
第二种办法需要 Vim 在编译时带上了 +clipboard。可以用 vim --version | grep clipboard 来查看,包含 +clipboard 则表示没问题,-clipboard 则表示不支持。OS X 自带的 Vim 是不支持的,用 brew 安装,或者 MacVim 是带了的。确认支持后,这样设置即可:

set clipboard+=unnamed
inoremap p

要注意的是,剪贴板与 Vim 共用后,Vim 中所有的复制操作都会修改剪贴板内容。而且 Vim 的删除命令也有复制的副作用,所以很可能一不小心就把剪贴板弄乱了。如果要将一个单词替换成剪贴板的内容,ce 是不行的,得用 vep。
第三种办法则这样实现:

if has("mac")

inoremap <C-p> <C-o>:r !pbpaste<CR>

endif

has("mac") 可以判断是不是 OS X,:r !pbpaste 则是读取 pbpaste 这个外部命令的输出,并插入到光标位置。
这个办法不跨平台,而且要调用外部命令,所以相对而言还是第二种比较好。但

还有一点美中不足的是,如果贴进来的代码和现有代码使用的缩进方式不一致(比如一个用空格,一个用 Tab),仍然会有问题。
好在 Vim 提供了 retab 这个命令,只要在粘贴后,再执行下它就行了:

inoremap p:retab
nnoremap p p:retab

然而故事的结尾,我还是用回了 PyCharm。
原因是 Vim 下最强大的代码补全插件 YouCompleteMe,也无法满足我的需求:from xxx import yyy 中,yyy 部分是无法补全的。
回到 PyCharm 后,我的感觉是配置 Vim 的过程像是自己在实现编辑器的各个功能,而 PyCharm 则是大部分都配好了,稍微改改就行了,很多时候比 Vim 更好用(例如随便一个编辑器都几乎不用为粘贴头疼)。当然,后者的缺点就是很多地方无法定制(虽然 Vim 也不是完全可定制的),例如我想在保存文件时删除末尾的空格,但是不处理 markdown 文件;还有复制粘贴时,按照当前文件的类型,自动重新格式化 Tab 和 Space。
为了尽量追上 Vim 的速度,还不得不再记一些快捷键(例如 Shift + CMD + o 和 CMD + e 可以快速打开文件),禁用没用的插件,隐藏编辑区和 Tab 区以外的区域等。
此外还发现 PyCharm 的 Vim 插件可以读取 ~/.ideavimrc 配置文件,不过基本只能定义 map,其中最需要加上的是设置是:

vnoremap < >gv

即选择一个区域后,可以连续调整缩进;否则需要精确计算缩进次数,用 n> 和 n< 来替代。

nnoremap :nohl 我就是用的nohl,再次搜索是自动高亮的,不用手动恢复

`