Vim 进阶——按键映射与 VimScript 脚本编程

一、按键映射

Vim 中的快捷键绑定可以通过以下命令配置:

  • :imap:只在 Insert 模式下生效的快捷键
  • :cmap:只在 Command-line 模式下生效
  • :nmap:只在 Normal 模式下生效
  • :vmap:只在 Visual 模式下生效
  • :map:在以上所有模式下生效
  • :noremap:包含 :inoremap:nnoremap 等,非递归映射

PS:关于递归映射,如 :map a b:map b c。根据按键映射之间的传递,则有 a -> c 的关系。而 nore 则用于禁止这种递归行为。

做快捷键映射时,各功能按键在 Vim 中的名称如下:

名称 对应按键
<BS> 退格键
<Tab> 制表键
<CR><Enter><Return> 回车
<Esc> Escape
<Space> 空格键
<Up> 上方向键
<Down> 下方向键
<Left> 左方向键
<Right> 右方向键
<F1> - <F12> 功能键 F1 到 F12
#1, #2..#9,#0 F1 到 F10
<Insert> Insert
<Del> Delete
<Home> Home
<End> End
<PageUp> 上翻页
<PageDown> 下翻页
示例
1
2
3
4
5
6
7
8
" save file (ctrl-s)
:map <C-s> :w<cr>
" copy selected text (ctrl-c)
:vmap <C-c> y
" Paste clipboard contents (ctrl-v)
:imap <C-v> <esc>P
" cut selected text (ctrl-x)
:vmap <C-x> x

:map <C-s> :w<cr> 表示将 Ctrl+S 组合键映射为 :w<cr>(保存修改,写入文件)
其中 <C-s> 在 Vim 中即表示 Ctrl+S,类似的用法还有 <A-s>Alt+S)、<M-s>Meta+S)、<C-S-s>Ctrl+Shift+S)等。
:w 后面的 <cr> 表示按下回车键。如命令最后没有加上 <cr>,则按下组合键 <C-s> 后只会将对应的 :w 输入到命令栏而不执行。

上面映射的保存文件功能还可以更加细化一些::imap <C-s> <esc>:w<cr>a
即该组合键适用于插入模式,按下 Ctrl+S 意味着会依照如下顺序执行命令:

  • <esc>:退出插入模式
  • :w<cr>:将之前的修改写入文件
  • a:回到插入模式继续编辑文件

对于 Gvim 下的按键映射,还可以调出对话框完成 Opensave-as 功能:

1
2
3
4
"Open new file dialog (ctrl-n)
:map <C-n> :browse confirm e<cr>
"Open save-as dialog (ctrl-shift-s)
:map <C-S-s> :browse confirm saveas<cr>

对于依次按下多个按键的组合键,如::map $1 :MyFunction1()<cr>
当按下 $ 键后,Vim 会等待一秒钟,若一秒钟之内又按下了 1 键,则执行映射的 MyFunction1() 函数;若一秒钟之内没有任何按键按下,则执行 $ 键原本的功能(移动光标到行尾)。

二、Vim 脚本

Vim 脚本是指用 VimScript 编写的为 Vim 添加自定义功能的单独的文件。如下面的 hello.vim 脚本:

1
2
3
4
5
6
7
8
" hello.vim
function! SayHello()
echo 'Hello, World!'
endfunction

command! Hello call SayHello()

nnoremap Q :Hello<CR>

其中 function! 用于定义 SayHello() 函数,command! 用于将调用该函数的行为绑定给 Hello 命令,nnoremap Q 则用于将 :Hello 命令的执行绑定给键盘上的 Q 按键。

在 Vim 中使用 :source 命令导入刚刚创建的脚本::source hello.vim
之后执行 :Hello 命令或者按下 Q 按键即可在命令栏输出 Hello, World! 字符串。

变量

变量的赋值使用 :let 命令:

1
2
3
4
:let mystring = "a string"
:let mynumber = 123
:let mylist = [1, 2, "three", 0x04 ,["five", "six"]]
:let mydict = {1: "one", 2: "two", "others": {3: "three", 4: "four"}}

Vim 脚本中支持以下类型的变量:

  • String:字符串,如 "this is a string"
  • Number:数字,包含十进制(123)、十六进制(0x8A)和八进制(012
  • List:有序列表,列表项支持多种数据类型混合
  • Dictionary:字典,无序键值对
  • Funcref:函数引用

PS:不同进制的数字之间可以直接进行算术运算
由单引号(')包裹的字符串不会转义 \n 等转义字符

关于字符串和数字之间的自动类型转换,参考以下规则:

Input (type) Result (type)
“hello” . “world” “hello world” (string)
“number” . 123 “number 123” (string)
“123” + 10 133 (number)
“123” - 10 . “hits” “113 hits” (string)
“123” - 10 + “hits” 113 (number)
变量作用域

Vim 中变量的作用域通过不同的前缀来指定:

  • v:Vim 预定义的全局作用域
  • g:全局变量前缀
  • b:Buffer 范围内生效的变量
  • t:Tab 范围内生效的变量
  • w:Window 范围内生效的变量
  • l:即 local,Function 范围内生效
  • s:即 source,通过 :source 载入的当前脚本内生效
  • a:即 argument,用来修饰函数的参数

当变量没有任何作用域前缀修饰时,默认为全局变量(除非该变量在函数内部定义)。

变量作用域前缀的使用参考如下脚本:

1
2
3
4
5
6
7
8
9
10
11
let g:sum = 10
function SumNumbers(num1, num2)
let l:sum = a:num1 + a:num2
if g:sum < l:sum
let g:sum = l:sum
endif
return l:sum
endfunction

echo SumNumbers(3, 4)
echo g:sum

条件语句

Vim 脚本中的 if-else 语句语法格式如下:

1
2
3
4
5
if condition1
code-to-execute-if-condition1-is-true
elseif condition2
code-to-execute-if-condition2-is-true
endif

其中判断条件 condition 可以有以下几种形式:

  • val1 == val2
  • val1 != val2
  • val1 > val1
  • val1 < val2
  • val1 <= val2
  • val1 >= val2
  • str1 =~ str2
  • str1 !~ str2

字符串比较中的 str2 可以是某个模式,支持正则表达式。

if-else 语句的使用可以参考如下脚本,根据当前时间切换不同的配色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
" note addition of zero
" this guarantees return from function is numeric
let currentHour = strftime ("%H")
echo "currentHour is " currentHour
if currentHour < 6 + 0
colorscheme darkblue
echo "setting colorscheme to darkblue"
elseif currentHour < 12 + 0
colorscheme morning
echo "setting colorscheme to morning"
elseif currentHour < 18 + 0
colorscheme shine
echo "setting colorscheme to shine"
else
colorscheme evening
echo "setting colorscheme to evening"
endif

循环

For 循环的语法格式如下:

1
2
3
for var in range
do-something
endfor


1
2
3
for var in list
do-somthing
endfor

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
" range
for myvar in range(1,10)
echo myvar
endfor

" list
let mylist = ['a','b','c','d','e','f','g','h','i','j','k']
for itemvar in mylist
echo itemvar
endfor

" dictionary
let mydict = {"a": "apple", "b":"banana", "c": "citrus" }
for keyvar in keys(mydict)
echo mydict[keyvar]
endfor

While 循环的语法格式:

1
2
3
while condition
execute-this-code
endwhile

代码示例:

1
2
3
4
5
let x=0
while x <= 5
echo "x is now " x
let x+=1
endwhile

列表与字典

关于 Vim 中列表与字典的操作,可以参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let mylist = [1, 2, "three"]
echo mylist[2]
" => three

let mylist2 = [[1, 2, 3], ["four", "five", "six"]]
echo mylist2[1][0]
" => four
echo mylist2[0][-1]
" => 3

let mylist3 = [1, 2, 3, 4]
call add(mylist3, [5, 6])
echo mylist3
" => [1, 2, 3, 4, [5, 6]]

let mylist4 = [1, 2, 3, 4]
let mylist4 = mylist4 + [5, 6]
echo mylist4
" => [1, 2, 3, 4, 5, 6]

let mylist5 = [1, 2, 3, 4]
call remove(mylist5, 3)
echo mylist5
" => [1, 2, 3]

let mydict = {'banana': 'yellow', 'apple': 'green'}
echo mydict['banana']
" => yellow
echo mydict.apple
" => green

get map split join

可以对列表或字典中的值应用某个函数(如 joinmap 等)以完成特定的需求,常见示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let a = split("one two")
echo a
" => one

let mylist = ["one", "two", "three"]
call map(mylist, '"<" . v:val . ">"')
echo mylist
" => ['<one>', '<two>', '<three>']

let mylist2 = ["one", "two", "three"]
echo get(mylist2, 2, "none")
" => three
echo get(mylist2, 3, "none")
" => none

let mylist3 = ["one", "two", "three"]
let mystring = join(mylist3, "+")
echo mystring
" => one+two+three

更复杂的示例(好神奇,没看懂。。。):

1
2
3
4
5
6
7
8
let mynumbers = {0:'zero', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five', 6:'six', 7:'seven', 8:'eight', 9:'nine'}

function mynumbers.convert(numb) dict
return join(map(split(a:numb, '\zs'), 'get(self, v:val, "unknown")'))
endfunction

echo mynumbers.convert(12345)
" => one two three four five

函数

Vim 中定义函数的语法如下:

1
2
3
function Name(arg1, arg2,...argN) keyword
code-to-execute-when-function-is-called
endfunction

所有在函数体中定义的变量只在该函数内部可见,即作用域为 local。如果需要使用函数外部的变量,可以将其作为参数传递给函数,或者直接调用(需要在该变量名前加上全局作用域前缀 g:)。
函数定义代码中使用由参数传递的变量时,需要加上 a: 作用域前缀。

参数列表

参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function PrintSum(num1, num2, ...)
let sum = a:num1 + a:num2
let argnum = 1
while argnum <= a:0
let sum += a:{argnum}
let argnum += 1
endwhile
echo "The sum is" sum
return sum
endfunction

let sum = PrintSum(1, 2, 3, 4)
" => The sum is 10

注意代码中 a:0(不定参数的长度)和 a:{argnum}(第 argnum 个额外参数)的用法。

此外还可以通过 a:000 以列表的方式获取所有额外的参数:

1
2
3
4
5
6
7
8
9
10
11
function PrintSum(num1, num2, ...)
let sum = a:num1 + a:num2
for arg in a:000
let sum += arg
endfor
echo "The sum is" sum
return sum
endfunction

let sum = PrintSum(1, 2, 3, 4)
" => The sum is 10

综合示例

根据当前时间自动切换 Vim 配色的脚本:

1
2
3
4
5
6
7
8
9
10
let g:Favcolorschemes = ["darkblue", "morning", "shine", "evening"]

function SetTimeOfDayColors()
" currentHour will be 0, 1, 2 or 3
let CurrentHour = (strftime("%H") + 0) / 6
execute "colorscheme " . g:Favcolorschemes[CurrentHour]
echo "set color scheme to " . g:Favcolorschemes[CurrentHour]
endfunction

call SetTimeOfDayColors()

三、Autocommands

Autocommands 即在特定条件下自动执行的命令,这些命令包括所有合法的 Vim 命令。
Vim 定义了一些事件event)作为触发命令自动执行的开关,常见的 event 如下:

  • BufNewFile:在开始编辑一个新的文件时触发
  • BufReadPre:在 Vim 移动到一个新的 buffer 前触发
  • BufRead, BufReadPost:开始编辑新的 buffer 时,读取文件之后触发
  • BufWrite, BufWritePre:在将 buffer 内容写入文件之前触发
  • FileType:在确定了文件类型(filetype)之后触发
  • VimResized:更改 Vim 的窗口大小后触发
  • WinEnter, WinLeave:进入或离开某个 Vim 窗口时触发
  • CursorMoved, CursorMovedI:Normal 或 Insert 模式下,光标移动后触发
Autocommands 代码示例
1
2
3
4
5
augroup demo
autocmd!
autocmd BufReadPost * echo 'Reading: ' . expand('<afile>')
autocmd BufWritePost * echo 'Writing: ' . expand('<afile>')
augroup END

上述脚本会添加如下功能:
打开文件时 Vim 命令栏输出 “Reading: “;在使用 :w 等命令保存文件时,命令栏输出 “Writing: “。

又如根据源文件类型设置不同的缩进风格:

1
2
3
filetype on
autocmd FileType ruby setlocal tabstop=2 softtabstop=2 shiftwidth=2 expandtab
autocmd FileType javascript setlocal ts=4 sts=4 sw=4 noet

参考资料

Hacking Vim 7.2
Modern Vim