155. VIM 从偶尔到日常

第22章 Vimrc

在先前的章节中,您学习了如何使用Vim。在本章,您将学习如何组织和配置Vimrc。

Vim如何找到Vimrc

对于Vimrc,常见的理解是在根目录下添加一个 .vimrc 点文件(根据您使用的操作系统,文件路径名可能不同)。

实际上,Vim在多个地方查找vimrc文件。下面是Vim检查的路径:

  • $VIMINIT
  • $HOME/.vimrc
  • $HOME/.vim/vimrc
  • $EXINIT
  • $HOME/.exrc
  • $VIMRUNTIME/default.vim

当您启动Vim时,它将在上面列出的6个位置按顺序检查vimrc文件,第一个被找到的vimrc文件将被加载,而其余的将被忽略。

首先,Vim将查找环境变量 $VIMINIT。如果没有找到,Vim将检查 $HOME/.vimrc。如果还没找到,VIm就检查 $HOME/.vim/vimrc。如果Vim找到了vimrc文件,它就停止查找,并使用 $HOME/.vim/vimrc

关于第一个位置,$VIMINIT 是一个环境变量。默认情况下它是未定义的。如果您想将 ~/dotfiles/testvimrc 作为 $VIMINTI 的值,您可以创建一个包含那个vimrc路径的环境变量。当您运行 export VIMINIT='let $MYVIMRC="$HOME/dotfiles/testvimrc" | source $MYVIMRC'后,VIm将使用 ~/dotfiles/testvimrc 作为您的vimrc文件。

第二个位置,$HOME/.vimrc 是很多Vim用户习惯使用的路径。$HOME 大部分情况下是您的根目录(~)。如果您有一个 ~/.vimrc 文件,Vim将使用它作为您的vimrc文件。

第三个,$HOME/.vim/vimrc,位于 ~/.vim 目录中。您可能已经有了一个 ~/.vim 目录用于存放插件、自定义脚本、或视图文件。注意这里的vimrc文件名没有“点”($HOME/.vim/.vimrc 不会被识别,但 $HOME/.vim/vimrc能被识别)。

第四个,$EXINIT 工作方式与 $VIMINIT 类似。

第五个,$HOME/.exrc 工作方式与 $HOME/.vimrc 类似。

第六个,$VIMRUNTIME/defaults.vim 是Vim编译时自带的默认vimrc文件。在我的电脑中,我是使用Homebrew安装的Vim8.2,所以我的路径是(/usr/local/share/vim/vim82)。如果Vim在前5个位置都没有找到vimrc文件,它将使用这个Vim自带的vimrc文件。

在本章剩余部分,我将假设vimrc使用的路径是 ~/.vimrc

应该把什么放在Vimrc中?

我刚开始配置Vimrc时,曾问过一个问题,“我究竟该把什么放在Vimrc文件中?”。

答案是,“任何您想放的东西”。 直接复制粘贴别人的vimrc文件的确是一个诱惑,但您应当抵制这个诱惑。如果您仍然坚持使用别人的vimrc文件,确保您知道这个vimrc干了什么,为什么他/她要用这些设置?以及他/她如何使用这些设置?还有最重要的是,这个vimrc文件是否符合你的实际需要?别人使用并不代表您也要使用。

Vimrc基础内容

简单地说,一个vimrc是以下内容的集合:

  • 插件
  • 设置
  • 自定义函数
  • 自定义命令
  • 键盘映射

当然还有一些上面没有提到的内容,但总体说,已经涵盖了绝大部分使用场景。

插件

在前面的章节中,我曾提到很多不同的插件,比如fzf.vim, vim-mundo, 还有 vim-fugitive.

十年前,管理插件插件是一个噩梦。但随着很多现代插件管理器的开发,现在安装插件可以在几秒内完成。我现在正在使用vim-plug作为我的插件管理器,所以我在本节中将使用它。相关概念和其他流行的插件管理器应该是类似的。我强烈建议您多试试几个插件管理器,比如:

除了上面列出的,还有很多插件管理器,可以随便看看。要想安装 vim-plug,如果您使用的是Unix,运行:

1
curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

要添加新的插件,将您的插件名(比如,Plug 'github-username/repository-name') 放置在 call plug#begin()call plug#end() 之间的行中. 所以,如果您想安装 emmet-vimnerdtree,将下面的片段放到您的vimrc中:

1
2
3
4
call plug#begin('~/.vim/plugged')
Plug 'mattn/emmet-vim'
Plug 'preservim/nerdtree'
call plug#end()

然后保存修改,加载当前vimrc (:source %), 然后运行 :PlugInstall 安装插件。

如果以后您想删除不使用的插件,您只需将插件名从 call 代码块之间移除,保存并加载,然后运行 :PlugClean 命令将它从机器上删除。

Vim 8 有自己的内置包管理器。您可以查阅 :h packages 了解更多信息。在后面一章中,我将向您展示如何使用它。

设置

在任意一个vimrc文件中都可以看到大量的 set 选项。 如果您在命令行模式中运行 set 命令,它只是暂时的。当您关闭Vim,设置就会丢失。比如,为了避免您每次运行Vim时都必须在命令行模式运行 :set relativenumber number 命令,您可以将这个命令添加在vimrc中:

1
set relativenumber number

有一些设置需要您赋予一个值,比如 set tabstop=2。想了解一个设置可以接收什么类型的值,可以查看帮助页。

您也可以使用 let 来代替 set(==确保在选项前添加一个 &号==)。使用 let ,您可以使用表达式进行赋值。比如,要想仅当某个路径存在时,才将该路径赋予 'dictionary' 选项:

1
2
3
4
5
let s:english_dict = "/usr/share/dict/words"

if filereadable(s:english_dict)
let &dictionary=s:english_dict
endif

在后面的章节中您将了解关于Vimscript赋值和条件的知识。

要查看Vim中所有可用的选项,查阅 :h E355

自定义函数

Vimrc是一个很好的用来放置自定义函数的地方。在后面的章节中您将学习如何写您自己的Vimscript函数。

自定义命令

您可以使用 command 创建一个自定义命令行命令。

比如,创建一个用于显示今天日期的基本命令 GimmeDate

1
:command! GimmeDate echo call("strftime", ["%F"])

当您运行 :GimmeDate 时,Vim将显示一个类似 “2021-01-1”的日期。

要创建一个可以接收输入的基本命令,您可以使用 <args> 。如果您想向 GimmeDate 传递一个时间/日期格式参数:

1
2
3
4
5
6
7
:command! GimmeDate echo call("strftime", [<args>])

:GimmeDate "%F"
" 2020-01-01

:GimmeDate "%H:%M"
" 11:30

如果您想限定参数的数目,您可以使用 -nargs 标志。-nargs=0 表示没有参数,-nargs=1 表示传递1个参数,-nargs=+ 表示至少1个参数,-nargs=* 表示传递任意数量的参数,-nargs=? 表示传递0个或1个参数。如果您想传递n个参数,使用 -nargs=n(这里 n 是一个任意整数)。

<args> 有两个变体:<f-args><q-args> 。前者用来向Vimscript函数传递参数,后者用来将用户输入自动转换为字符串。

使用 args:

1
2
3
4
5
6
:command! -nargs=1 Hello echo "Hello " . <args>
:Hello "Iggy"
" returns 'Hello Iggy'

:Hello Iggy
" Undefined variable error

使用 q-args:

1
2
3
:command! -nargs=1 Hello echo "Hello " . <q-args>
:Hello Iggy
" returns 'Hello Iggy'

使用 f-args:

1
2
3
4
5
6
7
8
:function! PrintHello(person1, person2)
: echo "Hello " . a:person1 . " and " . a:person2
:endfunction

:command! -nargs=* Hello call PrintHello(<f-args>)

:Hello Iggy1 Iggy2
" returns "Hello Iggy1 and Iggy2"

当您学了关于Vimscript函数的章节后,上面的函数将更有意义。

查阅 :h command:args 了解更多关于command和args的信息。

键盘映射

如果您发现您重复地执行一些相同的复杂操作,那么为这些复杂操作建立一个键盘映射将会很有用:

比如,在我的vimrc文件中有2个键盘映射:

1
2
3
nnoremap <silent> <C-f> :GFiles<CR>

nnoremap <Leader>tn :call ToggleNumber()<CR>

在第一个中,我将 Ctrl-F 映射到 fzf.vim 插件的 :Gfiles 命令(快速搜索Git文件)上。在第二个中,我将 <leader>tn 映射到调用一个自定义函数 ToggleNumber (切换 norelativenumberrelativenumber 选项)。Ctrl-f 映射覆盖了Vim的原生的页面滚动。如果发生冲突,您的映射将会覆盖Vim的设置。因为从几乎从来不用Vim原生的页面滚动功能,所以我认为可以安全地覆盖它。

另外,在 <Leader>tn 中的 “leader” 键到底是什么?

Vim有一个leader键用来辅助键盘映射。比如,我将 <leader>tn 映射为运行 ToggleNumber() 函数。如果没有leader键,我可能会用 tn,但Vim中的 t 已经用做其他功能(”till”搜索导航命令)了。有了leader键,我现在先按定义好的leader键作为开头,然后按 tn,而不用干扰已经存在的命令。您可以设置leader键作为您映射的连续按键的第一个按键。默认Vim使用反斜杠作为leader键(所以 <Leader>tn 会变成 “反斜杠-t-n”)。

我个人喜欢使用空格 <Space> 作为leader键,代替默认的反斜杠。要想改变您的leader键,将下面的文本添加到您的vimrc中:

1
let mapleader = "\<space>"

上面的 nnoremap 命令可以分解为三个部分:

  • n 表示普通模式。
  • nore 表示禁止递归。
  • map 是键盘映射命令。

如果不想使用 nnoremap,您至少也得使用 nmap (nmap <silent> <C-f> :Gfiles<CR>)。但是,最好还是使用禁止递归的版本,这样是为了避免键盘映射时潜在的无限循环风险。

如果您进行键盘映射时不使用禁止递归,下面例子演示了会发生什么。假设您想给 B 添加一个键盘映射,用来在一行的末尾添加一个分号,然后跳回前一个词组(回想一下,B 是Vim普通模式的一个导航命令,用来跳回前一个词组)。

1
nmap B A;<esc>B

当您按下 B …哦豁,Vim开始失控了,开始无止尽的添加;(用 Ctrl-c终止)。为什么会发生这样的情况?因为在键盘映射 A;<esc>B中,这个 B不再是Vim原生的导航命令,它已经被映射到您刚才创建的键盘映射中了。这是您实际上执行的操作序列:

1
A;<esc>A;<esc>A;<esc>A;esc>...

要解决这个问题,您需要指定键盘映射禁止递归:

1
nnoremap B A;<esc>B

现在再按一下 B 试试。这一次它成功地在行尾添加了一个 ;,然后跳回到前一个词组。这个映射中的 B 就表示Vim原生的 B了。

Vim针对不同的模式有不同的键盘映射命令。如果您想创建一个插入模式下的键盘映射 jk,用来退出插入模式:

1
inoremap jk <esc>

其他模式的键盘映射命令有:map(普通、可视、选择、以及操作符等待模式), vmap(可视、选择), smap(选择), xmap(可视), omap(操作符等待模式), map!(插入、命令行), lmap(插入,命令行,Lang-arg模式), cmap(命令行), 还有tmap(终端任务)。在这里我不会详细的讲解它们,要了解更多信息,查阅 :h map.txt

创建最直观、最一致、最易于记忆的键盘映射。

组织管理Vimrc

一段时候键,您的vimrc文件就会变大且复杂得难以阅读。有两种方法让您的vimrc文件保持整洁:

  • 将您的vimrc文件划分为几个文件
  • 折叠您的vimrc文件

划分您的vimrc

您可以使用Vim的 :source 命令将您的vimrc文件划分为多个文件。这个命令可以根据给定的文件参数,读取文件中的命令行命令。

让我们在 ~/.vim 下创建一个子文件夹,取名为 /settings~/.vim/settings)。名字可以取为任意您喜欢的名字。

然后你在这个文件夹下创建4个文件:

  • 第三方插件 (~/.vim/settings/plugins.vim).
  • 通用设置 (~/.vim/settings/configs.vim).
  • 自定义函数 (~/.vim/settings/functions.vim).
  • 键盘映射 (~/.vim/settings/mappings.vim) .

~/.vimrc 里面添加:

1
2
3
4
source $HOME/.vim/settings/plugins.vim
source $HOME/.vim/settings/configs.vim
source $HOME/.vim/settings/functions.vim
source $HOME/.vim/settings/mappings.vim

~/.vim/settings/plugins.vim 里面:

1
2
3
4
call plug#begin('~/.vim/plugged')
Plug 'mattn/emmet-vim'
Plug 'preservim/nerdtree'
call plug#end()

~/.vim/settings/configs.vim 里面:

1
2
3
set nocompatible
set relativenumber
set number

~/.vim/settings/functions.vim 里面:

1
2
3
4
5
6
7
function! ToggleNumber()
if(&relativenumber == 1)
set norelativenumber
else
set relativenumber
endif
endfunc

~/.vim/settings/mappings.vim 里面:

1
2
3
inoremap jk <esc>
nnoremap <silent> <C-f> :GFiles<CR>
nnoremap <Leader>tn :call ToggleNumber()<CR>

这样您的vimrc文件依然能够正常工作,但现在它只有4行了。

使用这样的设置,您可以轻易知道到哪去修改配置。如果您要添加一些键盘映射,就将它们添加在 /mappings.vim 文件中。以后,当您的vimrc变大时,您总是可以新建几个子文件来缩小它的大小。比如,如果您想为主题配色创建相关设置,您可以添加 ~/.vim/settings/themes.vim

保持单独的一个Vimrc文件

如果您倾向于保持一个单独的vimrc文件,以使它更加便于携带,您可以使用标志折叠让它保持有序。在vimrc文件的顶部添加一下内容:

1
2
3
4
5
6
" setup folds {{{
augroup filetype_vim
autocmd!
autocmd FileType vim setlocal foldmethod=marker
augroup END
" }}}

Vim能够检测当前buffer所属的文件类型 (:set filetype?). 如果发现属于 vim 类型,您可以使用标志折叠。回想一个标志折叠的用法,它使用 {{{` 和 `}}} 来指明折叠的开始和结束。

添加 {{{` 和 `}}} 标志将您的vimrc文件其他部分折叠起来。(别忘了使用 " 对标志进行注释):

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
31
32
33
34
35
" setup folds {{{
augroup filetype_vim
autocmd!
autocmd FileType vim setlocal foldmethod=marker
augroup END
" }}}

" plugins {{{
call plug#begin('~/.vim/plugged')
Plug 'mattn/emmet-vim'
Plug 'preservim/nerdtree'
call plug#end()
" }}}

" configs {{{
set nocompatible
set relativenumber
set number
" }}}

" functions {{{
function! ToggleNumber()
if(&relativenumber == 1)
set norelativenumber
else
set relativenumber
endif
endfunc
" }}}

" mappings {{{
inoremap jk <esc>
nnoremap <silent> <C-f> :GFiles<CR>
nnoremap <Leader>tn :call ToggleNumber()<CR>
" }}}

您的vimrc文件将会看起来类似下面:

1
2
3
4
5
6
7
8
9
+-- 6 lines: setup folds -----

+-- 6 lines: plugins ---------

+-- 5 lines: configs ---------

+-- 9 lines: functions -------

+-- 5 lines: mappings --------

启动Vim时加载/不加载Vimrc和插件

如果您要启动Vim时,既不加载Vimrc,也不加载插件,运行:

1
vim -u NONE

如果您要启动Vim时,不加载Vimrc,但加载插件,运行:

1
vim -u NORC

如果您要启动Vim时,加载Vimrc,但不加载插件,运行

1
vim --noplugin

如果您要Vim启动加载一个 其他的 vimrc, 比如 ~/.vimrc-backup, 运行:

1
vim -u ~/.vimrc-backup

聪明地配置Vimrc

Vimrc是定制Vim时的一个重要组件,学习构建您的Vimrc最好是首先阅读他人的vimrc文件,然后逐渐地建立自己的。最好的vimrc并不是谁谁谁使用的,而是最适合您的工作需要和编辑风格的。

第23章 Vim软件包

在前面的章节中,我提到使用第三方插件管理器来安装插件。从Vim 8开始,Vim自带了一个内置的插件管理器,名叫 软件包(packages)。在本章,您将学习如何使用Vim软件包来安装插件。

要看您的Vim编译版本是否能够使用软件包,运行 :version。然后查看是否有 +packages属性。另外,您也可以运行 :echo has('packages')(如果返回1,表示可以使用软件包)。

包目录

在根目录下查看您是否有一个 ~/.vim 文件夹。如果没有就新建一个。在文件夹里面,创建一个子文件夹取名 pack(~/.vim/pack/)。Vim会在这个子文件夹内自动搜索插件。

两种加载方式

Vim软件包有两种加载机制:自动加载和手动加载。

自动加载

要想让Vim启动时自动加载插件,你需要将它们放置在 start/子目录中。路径看起来像这样:

1
~/.vim/pack/*/start/

现在您可能会问,为什么在pack/start/ 之间有一个 * ?这个星号可以是任意名字。让我们将它取为packdemo/

1
~/.vim/pack/packdemo/start/

记住,如果您忽略这一点,用下面的路径代替的话:

1
~/.vim/pack/start/

软件包系统是不会正常工作的。 必须在pack/start/之间添加一个名字才能正常运行。

在这个示例中,让我们尝试安装 [NERDTree](https://github.com/preservim/nThe package system won’t work. It is imperative to put a name between pack/ and start/.erdtree) 插件。用任意方法进入 start/ 目录(cd ~/.vim/pack/packdemo/start/),然后将NERDTree的仓库克隆下来:

1
git clone https://github.com/preservim/nerdtree.git

完成了!您已经完成了安装。下一次您启动Vim,您可以立即执行 NERDTree 命令 :NERDTreeToggle

~/.vim/pack/*/start/ 目录中,您想克隆多少插件仓库就克隆多少。Vim将会自动加载每一个插件。如果您删除了克隆的仓库(rm -rf nerdtree),那么插件就失效了。

手动加载

要想在Vim启动时手动加载插件,您得将相关插件放置在 opt/ 目录中,类似于自动加载,这个路径看起来像这样:

1
~/.vim/pack/*/opt/

让我们继续使用前面的 packdemo/ 这个名字:

1
~/.vim/pack/packdemo/opt/

这一次,让我们安装killersheep 游戏(需要Vim8.2以上版本)。进入opt/ 目录(cd ~/.vim/pack/packdemo/opt/) 然后克隆仓库:

1
git clone https://github.com/vim/killersheep.git

启动Vim。执行游戏的命令是 :KillKillKill。试着运行一下。Vim将会提示这不是一个有效的编辑命令。您需要首先 手动 加载插件,运行:

1
:packadd killersheep

现在再运行一下 :KillKillKill 。命令已经可以使用了。

您可能好奇,“为什么我需要手动加载插件?启动时自动加载岂不是更好?”

很好的问题。有时候有些插件我们并不是所有的时候都在用,比如 KillerSheep 游戏。您可能不会想要加载10个不同的游戏导致Vim启动变慢。但是偶尔当您觉得乏味的时候,您可能想要玩几个游戏,使用手动加载一些非必须的插件。

您也可以使用这个方法有条件的加载插件。可能您同时使用了Neovim和Vim,有一些插件是为NeoVim优化过的。您可以添加类似下列的内容到您的vimrc中:

1
2
3
4
5
if has('nvim')
packadd! neovim-only-plugin
else
packadd! generic-vim-plugin
endif

组织管理软件包

回想一下,要使用Vim的软件包系统必须有以下需求:

1
~/.vim/pack/*/start/

或者:

1
~/.vim/pack/*/opt/

实际上,*星号可以使 任意 名字,这个名字就可以用来管理您的插件。假设您想将您的插件根据类型(颜色、语法、游戏)分组:

1
2
3
~/.vim/pack/colors/
~/.vim/pack/syntax/
~/.vim/pack/games/

您仍然可以使用各个目录下的 start/opt/

1
2
3
4
5
6
7
8
~/.vim/pack/colors/start/
~/.vim/pack/colors/opt/

~/.vim/pack/syntax/start/
~/.vim/pack/syntax/opt/

~/.vim/pack/games/start/
~/.vim/pack/games/opt/

聪明地添加插件

您可能好奇,Vim软件包是否让一些流行的插件管理器,比如 vim-pathogen, vundle.vim, dein.vim, a还有vim-plug面临淘汰?

答案永远是:“看情况而定。”

我仍然使用vim-plug,因为使用它添加、删除、更新插件很容易。如果您使用了很多插件,插件管理器的好处更加明显,因为使用它可以对很多插件进行同时更新。有些插件管理器同时也提供了一些异步功能。

如果您是极简主义者,可以尝试一下Vim软件包。如果您是一名插件重度使用者,您可能需要一个插件管理器。

第24章 Vim Runtime

在前面的章节中,我提到Vim会自动查找一些特殊的路径,比如在~/.vim/ 中的 pack/(第23章) compiler/(第19章)。这些都是Vim runtime路径的例子。

除了上面提到的两个,Vim还有更多runtime路径。在本章,您将学习关于Vim runtime路径的高层次概述。本章的目标是向您展示它们什么时候被调用。知道这些知识能够帮您更进一步理解和定制Vim。

Runtime路径

在一台Unix机器中,其中一个vim runtime路径就是 $HOME/.vim/ (如果您用的是其他操作系统,比如Windows,您的路径可能有所不同)。要查看不同的操作系统有什么样的runtime路径,查阅 :h runtimepath。在本章,我将使用 ~/.vim/ 作为默认的runtime路径。

Plugin脚本

Vim有一个runtime路径 plugin,每次Vim启动时都会执行这个路径中的所有脚本。不要把这个名字 “plugin” 和Vim的外部插件(external plugins,比如NERDTree, fzf.vim, 等)搞混了。

进入 ~/.vim/ 目录,然后创建 plugin/ 子目录。 创建两个文件: donut.vimchocolate.vim

~/.vim/plugin/donut.vim里面:

1
echo "donut!"

~/.vim/plugin/chocolate.vim里面:

1
echo "chocolate!"

现在关闭Vim。下次您启动Vim,您将会看到 "donut!":chocolate! 的显示。此 plugin runtime路径可以用来执行一些初始化脚本。

文件类型检测

在开始之前,为保证检测能正常运行,确保在您的vimrc中至少包含了下列的行:

1
filetype plugin indent on

查阅 :h filetype-overview 了解更多信息。本质上,这条代码开启Vim的文件类型检测。

当您打开一个新的文件,Vim通常知道这个文件是什么类型。如果您有一个文件 hello.rb,运行 :set filetype? 会返回正确的相应 filetype=ruby

Vim知道如何检测 “常见” 的文件类型(Ruby, Python, Javascript, 等)。但如果是一个自定义文件会怎样呢?您需要告诉Vim去检测它,并给它指派一个正确的文件类型。

有两种检测方法:使用文件名和使用文件内容

文件名检测

文件名检测使用一个文件的文件名来检测文件类型。当您打开 hello.rb文件时,Vim依靠扩展名 .rb 知道它是一个Ruby文件。

有两种方法实现文件名检测:一是使用 ftdetect runtime目录,二是使用 filetype.vim runtime文件。我们两个都看一看。

ftdetect/

让我们创建一个古怪(但优雅)的名字,hello.chocodonut。当您打开它后运行 :set filetype? ,因为它的后缀名不是常见的文件名,Vim不知道它是什么类型,会返回 filetype=

您需要指示Vim将所有以 .chocodonut结尾的文件设置为 “chocodonut”类型的文件。在runtime路径根目录(~/.vim/)创建一个子目录,名为 ftdetect/ 。在子目录里面,再创建一个名叫 chocodonut.vim 的文件(~/.vim/ftdetect/chocodonut.vim),在文件里面,添加:

1
autocmd BufNewFile,BufRead *.chocodonut set filetype=chocodonut

当您创建新buffer或打开新buffer时,事件BufNewFileBufRead 就会被触发。 *.chocodonut 意思是只有当新打开的buffer文件名后缀是 .chocodonut 时事件才会被触发。最后,set filetype=chocodonut 命令将文件类型设置为chocodonut类型。

重启Vim。新建一个 hello.chocodonut 文件然后运行 :set filetype?。它将返回 filetype=chocodonut.

好极了!只要您想,您可以将任意多的文件放置在 ftdetect/ 中。以后,如果您想扩展您的 donut 文件类型,你可以添加 ftdetect/strawberrydonut.vim, ftdetect/plaindonut.vim 等等。

在Vim中,实际上有两种方法设置文件类型。其中给一个是您刚刚使用的 set filetype=chocodonut。另一种方法是运行 setfiletype chocodonut。前一个命令 set filetype=chocodonut总是 设置文件类型为chocodonut。 而后者setfiletype chocodonut只有当文件类型尚未设置时,才会将文件类型设置为chocodonut。

文件类型文件

第二种文件类型检测需要你创建一个名为 filetype.vim的文件,并将它放置在根目录(~/.vim/filetype.vim)。在文件内添加一下内容:

1
autocmd BufNewFile,BufRead *.plaindonut set filetype=plaindonut

创建一个名为 hello.plaindonut 的文件。当你打开它后运行 :set filetype? Vim会显示正确的自定义文件类型 filetype=plaindonut

太好了,修改生效了。另外,如果您仔细看看 filetype.vim ,您会发现当您打开hello.plaindonut时,这个文件文件运行了多次。为防止这一点,您可以添加一个守卫,让主脚本只运行一次。更新 filetype.vim:

1
2
3
4
5
6
7
if exists("did_load_filetypes")
finish
endif

augroup donutfiletypedetection
autocmd! BufRead,BufNewFile *.plaindonut setfiletype plaindonut
augroup END

finish 是一个Vim命令,用来停止执行剩余的脚本。表达式"did_load_filetypes"不是 一个Vim内置函数。它实际上是$VIMRUNTIME/filetype.vim 中的一个全局变量。如果您好奇,运行:e $VIMRUNTIME/filetype.vim。您将会发现以下内容:

1
2
3
4
5
if exists("did_load_filetypes")
finish
endif

let did_load_filetypes = 1

当Vim调用这个文件时,它会定义 did_load_filetypes 变量,并将它设置为 1 。在Vim中,1 表示真。你可以试着读完 filetype.vim 剩余的内容,看看您是否能够理解当Vim调用它时干了什么。

文件类型脚本

让我们学习如何基于文件内容检测文件类型。

假设您有一个无扩展名的文件的集合。这些文件唯一相同的地方是,第一行都是以 “donutify” 开头。您现在想给这些文件指派一个 donut 的文件类型。创建新文件,起名为 sugardonut, glazeddonut, 还有 frieddonut(没有扩展名)。在每个文件中,添加下列内容:

1
donutify

当您在sugardonut中运行 :set filetype?,Vim无法知道应该给这个文件指派什么文件类型,会返回 filetype=

在runtime根目录,添加一个 scripts.vim 文件(~/.vim/scripts.vim),在文件中,添加一下内容:

1
2
3
4
5
6
7
if did_filetype()
finish
endif

if getline(1) =~ '^\\<donutify\\>'
setfiletype donut
endif

函数 getline(1) 返回文件第一行的内容。它检查第一行是否以 “donutify” 开头。函数 did_filetype() 是Vim的内置函数,当一个与文件类型相关的事件发生至少一次时,它返回真。它用来做守卫,防止文件类型事件反复运行。

打开文件 sugardonut 然后运行 :set filetype?,Vim现在返回 filetype=donut。如果您打开另外一个donut文件 (glazeddonutfrieddonut),Vim同样会将它们的文件类型定义为 donut 类型。

注意,scripts.vim 仅当Vim打开一个未知文件类型的文件时才会运行。如果Vim打开一个已知文件类型的文件,scripts.vim 将不会运行。

文件类型插件

如果您想让Vim仅当您打开一个 chocodonut 文件时才运行 chocodonut 相关的特殊脚本,而当您打开的是 plaindonut 文件时,Vim就不运行这些脚本。能否做到呢?

您可以使用文件类型插件runtime路径(~/.vim/ftplugin/)来完成这个功能。Vim会在这个目录中查找一个文件,这个文件的文件名与您打开的文件类型一样。创建一个文件,起名为chocodonut.vim (~/.vim/ftplugin/chocodonut.vim):

1
echo "Calling from chocodonut ftplugin"

创建另一个 ftplugin 文件,起名为plaindonut.vim (~/.vim/ftplugin/plaindonut.vim):

1
echo "Calling from plaindonut ftplugin"

现在,每次您打开一个 chocodonut 类型的文件时,Vim会运行 ~/.vim/ftplugin/chocodonut.vim中的脚本。每次您打开 plaindonut 类型的文件时,Vim会运行 ~/.vim/ftplugin/plaindonut.vim 中的脚本。

一个警告:每当一个buffer的文件类型被设置时(比如,set filetype=chocodonut),上述脚本就会运行一次。如果您打开3个不同的 chocodonut 文件,该脚本将运行 总共 3次。

缩进文件

Vim有一个 缩进runtime路径,其工作方式与ftplugin类似,Vim也会在这个目录中查找一个与打开的文件类型名字一样的文件。缩进runtime路径的目的是存储缩进相关的代码。如果您有文件 ~/.vim/indent/chocodonut.vim,它仅当您打开一个 chocodonut 类型的文件时执行。您可以将 chocodonut 文件中缩进相关的代码存储在这里。

颜色

Vim 有一个颜色runtime路径 (~/.vim/colors/) ,用来存储颜色主题。这个目录中的任何文件都会在命令行命令 :color 中显示出来。

如果您有一个文件 ~/.vim/colors/beautifulprettycolors.vim,当您运行 :color 然后按 Tab,您将会看到 beautifulprettycolors 出现在颜色选项中。 如果您想添加自己的颜色主题,就放在这个地方。

如果您想看其他人做的颜色主题,有一个好地方值得推荐:vimcolors

语法高亮

Vim有一个语法runtime路径 (~/.vim/syntax/),用来定义语法高亮。

假设您有一个文件 hello.chocodonut,在文件里面有以下内容:

1
2
(donut "tasty")
(donut "savory")

虽然Vim现在知道了正确的文件类型,但所有的文本都是相同的颜色。让我们添加语法高亮规则,使 “donut” 关键词高亮显示。创建一个新的 chocodonut 语法文件 ~/.vim/syntax/chocodonut.vim,在文件中添加:

1
2
3
syntax keyword donutKeyword donut

highlight link donutKeyword Keyword

现在重新打开 hello.chocodonut 文件,关键词 donut 已经高亮显示了。

本章不会详细介绍语法高亮。它是一个庞大的主题。如果您感兴趣,可以查阅 :h syntax.txt

vim-polyglot 插件非常的棒,它提供了很多流行的编程语言的语法高亮。

文档

如果您写了一个插件,您还得创建一个您自己的文档。您可以使用文档runtime路径完成这个。

让我们为 chocodonut 和 plaindonut 关键字创建一个基本文档。创建文件 donut.txt (~/.vim/doc/donut.txt)。在文件中,添加一下内容:

1
2
3
*chocodonut* Delicious chocolate donut

*plaindonut* No choco goodness but still delicious nonetheless

如果您试着搜索 chocodonutplaindonut (:h chocodonut:h plaindonut),您找不到任何东西。

首先,你需要运行 :helptags来创建新的帮助入口。运行 :helptags ~/.vim/doc/

现在,如果您运行 :h chocodonut:h plaindonut,您将找到上面那些新的帮助入口。注意,现在文件是只读的,而且类型是 “help”。

延时加载脚本

到现在,本章您学到的所有runtime路径都是自动运行的。如果您想手动加载一个脚本,可使用 autoload runtime路径。

创建一个目录名为 autoload(~/.vim/autoload/)。在目录中,创建一个新文件,起名为 tasty.vim (~/.vim/autoload/tasty.vim)。在文件中:

1
2
3
4
5
echo "tasty.vim global"

function tasty#donut()
echo "tasty#donut"
endfunction

注意,函数名是 tasty#donut 而不是 donut()。要想使用autoload功能,井号(#)是必须的。在使用autoload功能时,函数的命名惯例是:

1
2
3
function fileName#functionName()
...
endfunction

在本例中,文件名是 tasty.vim,而函数名是donut

要调用一个函数,可以使用 call 命令。让我们call这个函数 :call tasty#donut()

您第一次调用这个函数时,您应当会 同时 看到两条信息 (“tasty.vim global” 和 “tasty#donut”) 。后面再调用 tasty#donut 函数,将只会显示 “testy#donut”。

当您在Vim中打开一个文件,不像前面说的runtime路径,autoload脚本不会被自动加载。仅当您显式地调用 tasty#donut(),Vim才会查找文件tasty.vim,然后加载文件中的内容,包括函数 tasty#donut()。有些函数会占用大量资源,但我们又不常用,这时候 Autoload runtime路径就是最佳的解决方案。

您可以在autoload目录任意添加嵌套的目录。如果您有一个runtime路径 ~/.vim/autoload/one/two/three/tasty.vim,您可以使用:call one#two#three#tasty#donut()来调用函数。

After脚本

Vim有一个 after runtime路径 (~/.vim/after/) ,它的结构是 ~/.vim/的镜像。在此目录中的任何脚本都会最后执行,所以开发者通常使用这个路径来重载脚本。

比如,如果您想重载 plugin/chocolate.vim 中的脚本,您可以创建~/.vim/after/plugin/chocolate.vim来放置重载脚本。Vim将会先运行 ~/.vim/plugin/chocolate.vim然后运行 ~/.vim/after/plugin/chocolate.vim

$VIMRUNTIME

Vim有一个环境变量 $VIMRUNTIME 用来加载默认脚本和支持文件。您可以运行 :e $VIMRUNTIME查看。

它的结构应该看起来很熟悉。它包含的很多runtime路径都是我们本章前面学过的。

回想第22章,当您打开Vim时,它会在6个不同的位置查找vimrc文件。当时我说最后一个位置就是 $VIMRUNTIME/default.vim,如果Vim在前5个位置查找用户vimrc文件失败,就会使用default.vim 作为vimrc。

不知您是否尝试过,运行Vim是不加载比如vim-polyglot之类的语法插件,但您的文件依然有语法高亮?这是因为当Vim在runtime路径查找语法文件失败时,会从$VIMRUNTIME 的语法目录中查找语法文件。

查阅 :h $VIMRUNTIME了解更多信息。

Runtimepath选项

运行 :set runtimepath?,可以查看您的runtime路径。

如果您使用 Vim-Plug 或其他流行的第三方插件管理器,它应该会显示一个目录列表。比如,我的显示如下:

1
runtimepath=~/.vim,~/.vim/plugged/vim-signify,~/.vim/plugged/base16-vim,~/.vim/plugged/fzf.vim,~/.vim/plugged/fzf,~/.vim/plugged/vim-gutentags,~/.vim/plugged/tcomment_vim,~/.vim/plugged/emmet-vim,~/.vim/plugged/vim-fugitive,~/.vim/plugged/vim-sensible,~/.vim/plugged/lightline.vim, ...

插件管理器做了一件事,就是将每个插件添加到runtime路径中。每个runtime路径都有一个类似 ~/.vim/的目录结构。

如果您有一个目录 ~/box/of/donuts/,然后您想将这个目录添加到您的runtime路径中,您可以在vimrc中添加以下内容:

1
set rtp+=$HOME/box/of/donuts/

如果在 ~/box/of/donuts/ 里面,您有一个plugin目录 (~/box/of/donuts/plugin/hello.vim) 以及ftplugin目录 (~/box/of/donuts/ftplugin/chocodonut.vim),当您打开Vim时,Vim将会运行 plugin/hello.vim 中所有脚本。同样,当您打开一个 chocodonut 文件时,Vim 将会运行 ftplugin/chocodonut.vim

自己试着做一下:创建一个任意目录,然后将它添加到您的 runtimepath中。添加一些我们本章学到的runtime路径。确保它们按预期工作。

聪明地学习Runtime

花点时间阅读本章,还有认真研究一下这些runtime路径。看一下真实环境下runtime路径是如何使用的。浏览一下您最喜欢的Vim插件仓库,仔细研究一下它的目录结构,您应该能够理解它们中的绝大部分。试着领会重点并跟着做。现在您已经理解了Vim的目录结构,您可以准备学习Vimscript了。

第25章 Vimscript 基础和数据类型

Ex 模式

技术上来说,Vim 没有内置的交互式解释器 (REPL),但是有一个 Ex 模式,可以使 vim 像使用 REPL 一样、Ex 模式更现实命令行模式的拓展,即非停止的命令行模式

进入EX模式的方法:

  • Q
  • gQ

退出Ex模式

  • 输入: :visual

echo

类似于 pythonprint()

echom: 和 echo 相同, 不同支出在于 echom 会将结果保存到 message 历史记录中

  • 查看 message 历史记录

    :messages

  • 清空 mwssage 历史记录

    :message clear

数字

模式指的是==十进制==数字

十进制 Decimal

1, -1, -10

十六进制 Hexadecimal

0xoX 开头

二进制 Binary

0b0B 开头

八进制 Octal

0, 0o0O 开头

真假值

  • 真:非0
  • 假:0

数值运算

同 C++ 的数值运算

字符串

使用 双引号("") 或 单引号('') 包裹起来

字符串连接

使用 . 连接字符串

1
:echo "Hello" . "world"

字符串算数

当使用数字和字符串使用运算符 + - * / 进行算数运算时,vim会将字符串转化为数字

1
:echo "12 donuts" + 3

也可以是字符串与字符串进行数值计算

1
:echo "12 donuts" * "6 pastries"

要求

  • 数字字符必须是字符串的第一个字符

注意

  • 当使用浮点型进行运算时,将被转化成整数

数字与字符串连接

使用 . 运算符

注意

  • 不适用于浮点数

字符串作为条件时

  • 真:第一个字符为数字
  • 假:第一个字符不为数字

单引号与双引号

  • 单引号只会输出字面值
  • 双引号可以接受特殊字符,如: \n, \"

内置字符串程序

程序 功能
strlen() 获取字符串长度
str2nr() 将字符串转化为数字
str2float() 将字符串转化为浮点数
substitute() 字符串模式替换
可以结合 getline() 使用
其他函数 :h string-functions

getline(): 获取传入行号的文本

列表

类似于 Python 的列表,类型可以混合在一起

子列表

获取单个值

同 Python 的索引,类似于list[n]

切片

  • list[n:m] 获取 list 列表中的索引号 n 到 m 的元素 (包括m)
  • list[n:]: n到末尾
  • list[:m]: 从头到m

注: 若 m > list 的长度,那么就是获取 m 到末尾的元素

切片字符串

同list

数值运算

使用 +: 连接两个劫镖

函数

函数 功能
len() 获得列表长度
insert() 将元素传输到列表开始位置
remove() 删除列表元素
map()
filter()
用来过滤元素 ==????==
1
2
3
4
5
6
7
8
9
:let sweeterList = ["glazed", "chocolate", "strawberry"]
:call filter(sweeterList, 'v:val !~ "choco"')
:echo sweeterList
" returns ["glazed", "strawberry"]

:let sweetestList = ["chocolate", "glazed", "sugar"]
:call map(sweetestList, 'v:val . " donut"')
:echo sweetestList
" returns ['chocolate donut', 'glazed donut', 'sugar donut']

解包

像python

1
2
3
4
5
6
7
8
:let favoriteFlavor = ["chocolate", "glazed", "plain"]
:let [flavor1, flavor2, flavor3] = favoriteFlavor

:echo flavor1
" returns "chocolate"

:echo flavor2
" returns "glazed"

使用 ; 分配剩余的元素

1
2
3
4
5
6
7
8
:let favoriteFruits = ["apple", "banana", "lemon", "blueberry", "raspberry"]
:let [fruit1, fruit2; restFruits] = favoriteFruits

:echo fruit1
" returns "apple"

:echo restFruits
" returns ['lemon', 'blueberry', 'raspberry']

修改列表

修改一项

1
2
3
4
:let favoriteFlavor = ["chocolate", "glazed", "plain"]
:let favoriteFlavor[0] = "sugar"
:echo favoriteFlavor
" returns ['sugar', 'glazed', 'plain']

修改多项

1
2
3
4
:let favoriteFlavor = ["chocolate", "glazed", "plain"]
:let favoriteFlavor[2:] = ["strawberry", "chocolate"]
:echo favoriteFlavor
" returns ['chocolate', 'glazed', 'strawberry', 'chocolate']

字典

类似于 python

==区别==在于 vim 只使用字符串为“键”,即使使用数字,也会将数字转化为字符串

偷懒方法

使用 #{} 来表示

1
2
3
4
:let mealPlans = #{breakfast: "waffles", lunch: "pancakes", dinner: "donuts"}

:echo mealPlans
" returns {'lunch': 'pancakes', 'breakfast': 'waffles', 'dinner': 'donuts'}

要求

“键”的字符必须满足一下条件:

  • ASCII character.
  • Digit.
  • An underscore (_).
  • A hyphen (-).

访问字典

  • []

    1
    meal['breakfast']
  • .

    1
    meal.lunch

修改字典

  • []`

    1
    meal['breakfast'] = "..."
  • .

    1
    meal.lunch = "..."

内置函数

函数 功能
len() 获取字典长度
has_key() 获取字典的键
empty() 字典是否有键值对
remove() 移除键值对
items() 将键值对转化为列表:每个键值对为一个列表
filter() and map() 过滤作用

特殊基元

  • v:false
  • v:true
  • v:none
  • v:null

注:

  • v: 是 vim 内置的变量
  • 这些不常用
  • 常用如下
    • 布尔值:0(假);non-0 (真)
    • 空字符串:""

True

1
2
:echo json_encode({"test": v:true})
" returns {"test": true}

False

1
2
:echo json_encode({"test": v:false})
" returns {"test": false}

None

1
2
:echo json_encode({"test": v:none})
" returns {"test": null}

被解释成 null

Null

1
2
:echo json_encode({"test": v:null})
" returns {"test": null}

类似于 v:none

第26章 条件和循环

关系表达式

  • ==
  • !=
  • >
  • >=
  • <
  • <=

注意:这里的字符串仍被转换为数字

字符串逻辑运算

用关系运算符来比较字符串

  • =~:对字符串进行正则表达式匹配:可以模式匹配上
  • !~: 对字符串进行正则表达式匹配:不可以匹配上
  • ==: 完全匹配:相等
  • !=:完全匹配:不相等

注意:如果在设置了 set ignorecase,那么比较时也会忽略大小写

设置总是忽略大小写

  • 在末尾加一个 #,如:=~# (推荐:安全)

    1
    echo str =~# "hearty"
  • 在末尾加一个 ?,如:=~?

    1
    echo str =~? "hearty"

if

最小的 if 语句

1
2
3
if {clasuse}
{some expression}
endif

if 全貌

1
2
3
4
5
6
7
8
9
if {predicate1}
{expression1}
elseif {predicate2}
{expression2}
elseif {predicate3}
{expression3}
else
{expression4}
endif

三元表达式

1
{predicate} ? expressiontrue : expressionfalse

补充

  • background: 背景
  • strftime(): 目前时间

or

||

注意:短路运算

and

&&

注意:短路运算

for

方法1

1
2
3
4
5
let breakfasts = ["pancakes", "waffles", "eggs"]

for breakfast in breakfasts
echo breakfast
endfor

方法2

1
2
3
4
5
let meals = [["breakfast", "pancakes"], ["lunch", "fish"], ["dinner", "pasta"]]

for [meal_type, food] in meals
echo "I am having " . food . " for " . meal_type
endfor

方法3

1
2
3
4
let beverages = #{breakfast: "milk", lunch: "orange juice", dinner: "water"}
for beverage_type in keys(beverages)
echo "I am drinking " . beverages[beverage_type] . " for " . beverage_type
endfor

while

常见

1
2
3
4
5
let counter = 1
while counter < 5
echo "Counter is: " . counter
let counter += 1
endwhile

获得每行内容

1
2
3
4
5
6
7
let current_line = line(".")
let last_line = line("$")

while current_line <= last_line
echo getline(current_line)
let current_line += 1
endwhile

Error Handling

break

和 python 一样:中断循环

Continue

和 python 一样:跳过当前轮

try, finally, and catch

和 python 一样

  • finally: 总是被执行,且是最后被执行
  • catch: 捕获错误

  • throw:抛出一个错误

catch 捕捉的错误

:h :catch

1
2
3
4
5
6
7
8
catch /^Vim:Interrupt$/.             " catch interrupts (CTRL-C)
catch /^Vim\\%((\\a\\+)\\)\\=:E/. " catch all Vim errors
catch /^Vim\\%((\\a\\+)\\)\\=:/. " catch errors and interrupts
catch /^Vim(write):/. " catch all errors in :write
catch /^Vim\\%((\\a\\+)\\)\\=:E123:/ " catch error E123
catch /my-exception/. " catch user exception
catch /.*/ " catch everything
catch. " same as /.*/

第27章 变量范围

变量与常量

变量

定义变量

1
let val = "value"

let 定义的是==变量==

修改变量

当需要改变变量的值时,需要使用 let

1
2
let val = "value"
let val = "value1"

使用变量

1
echo val

常量

定义

使用 const

1
const val = "value"

变量源

  • 环境变量
  • 选项变量
  • 寄存器变量

环境变量

vim 可以访问==终端的环境变量==

语法

1
${Enviroment_variable}

示例

1
echo $SHELL

注意:不要使用 {},错误的用法: echo ${SHELL}

选项变量

这些变量也就是 .vimrc 中使用 set 设置的那些变量

访问

  • 方法1: 使用 &`

    1
    echo &background
  • 方法2: set background?

寄存器变量

访问

使用 @

更新寄存器的值

使用 let

示例

  • 访问寄存器 a

    1
    echo @a
  • 更新寄存器 a 的值

    1
    let @a .= " donut"

变量范围

变量范围 说明
g: Global variable
{nothing} Global variable
b: Buffer-local variable
w: Window-local variable
t: Tab-local variable
s: Sourced Vimscript variable
l: Function local variable
a: Function formal parameter variable
v: Built-in Vim variable

全局变量

范围

可以在任何地方使用

定义

  • let val = "value"
  • let g:val = "value"

使用

  • echo val
  • echo g:val

缓冲区变量

定义

添加前缀 b:

1
const b:donut = "chocoldate donut"

说明

定义的每个缓冲区变量仅在定义时所在的缓冲区有效,也就是说缓冲区变量的作用域是定义所在缓冲区

特殊缓冲区值

b:changedtick: 跟踪了在当前缓冲区所有的改变

窗口变量

定义

添加前缀 w:

1
let w:donut = "donut"

说明

类似于缓冲区变量,变量的作用域为定义的窗口

标签变量

定义

添加前缀 t:

1
let t:done

说明

类似于窗口变量和缓冲区变量

脚本变量

定义

使用前缀 s:

说明

仅在定义变量的脚本内部才可以访问到

Function Local and Function Formal Parameter Variable

定义

  • 函数局部变量:l:
  • 函数形参变量: a:

内置变量

说明

前缀为 v:

注意

无法定义这些变量

常见

变量 说明
v:version vim 版本
v:key contains the current item value when iterating through a dictionary.
v:val contains the current item value when running a map() or filter() operation.
v:true, v:false, v:null, and v:none special data types.

注意:查看其他的内置变量 :h vim-variable or :h v:.

第28章 函数

函数语法

1
2
3
function {FunctionNmae}()
{do-something}
endfunction

注意:

  • ==函数名必须以大写字母开头==
  • 函数名不能以数字开头

函数名小写字母开始

使用脚本变量前缀 s:,如:function s:tasty()

同名函数

不允许有重名函数

覆写函数

function 后面使用 !

1
2
3
function! Tasty()
echo "Tasty"
endfunction

覆写之后上面的函数就会没有。==覆写不是重载==哦

查看所有可用函数

使用 :function

查看某一个函数内容

:function Tasty

模式搜索函数

:function /pattern

查看函数位置

使用 :verbose:function 命令

  • 某一函数

    1
    :verbose function <FunctionName>
  • 模式匹配函数

    1
    :verbose function /<pattern>

删除函数

删除一个已有函数: :delfunction {FunctionName}

例如: :delfunction Tasty

函数返回值

  • 默认返回 0
  • return: 等价于返回0

函数参数

用法

使用 a:

示例

如函数 Tasty() 使用参数 food

1
2
3
function! Tasty(food)
return "Tasty" . a:food
endfunction

局部变量

用法

使用 l:

常见问题

问题 1

若在函数内部定义一个变量,那么这个变量默认为局部变量

1
2
3
4
function! Tasty()
let location = "tasty"
return location . " in Tasty"
endfunction

与下面代码等价,但是==没有下面的代码好==

1
2
3
4
function! Tasty()
let l:location = "tasty"
return l:location . " in Tasty"
endfunction

问题 2

1
2
3
4
5
6
7
function! Calories()
let count = "count"
return "I do not " . count . " my calories"
endfunction

echo Calories()
" throws an error

变量会与特殊变量同名,导致错误

错误根源:let count = "count"

调用函数

使用

纯粹调用函数

使用 call 调用函数

  • 命令行

    1
    :call Tasty()
  • 脚本

    1
    call Tasty()

You cannot call a command-line command with another command-line command.

你不能使用命令行命令调用命令行命令

使用函数返回值
使用 call("<FunctionName>", [<参数列表>])

[] 不可以省略

区别

  • :call 是命令行命令
  • call(): 函数
    • 第一个参数接收函数名
    • 第二个参数接收形参列表

默认参数

使用 =

示例

1
2
3
4
5
6
7
8
9
function! Breakfast(meal, beverage = "Milk")
return "I had " . a:meal . " and " . a:beverage . " for breakfast"
endfunction

echo Breakfast("Hash Browns")
" returns I had hash browns and milk for breakfast

echo Breakfast("Cereal", "Orange Juice")
" returns I had Cereal and Orange Juice for breakfast

可变参数

使用 ...

示例

1
2
3
function! Buffet(...)
return a:1
endfunction
  • a:0: 共有多少个参数
  • a:1 是第一个参数
  • a:2 是第二个参数
  • a:20 是第20个参数
  • a:000 是所有参数值组成的列表

a:0 示例

1
2
3
4
5
6
7
8
9
function! Buffet(...)
let l:food_counter = 1
let l:foods = ""
while l:food_counter <= a:0
let l:foods .= a:{l:food_counter} . " "
let l:food_counter += 1
endwhile
return l:foods
endfunction

a:000 + for

1
2
3
4
5
6
7
8
9
10
function! Buffet(...)
let l:foods = ""
for food_item in a:000
let l:foods .= food_item . " "
endfor
return l:foods
endfunction

echo Buffet("Noodles", "Sushi", "Ice cream", "Tofu", "Mochi")
" returns Noodles Sushi Ice cream Tofu Mochi

Range

在定义函数的第一行最后加入 range

效果

会给该函数添加两个特定变量:

  • a:firstline: 所选范围的第一行的行号
  • b:lastline: 所选范围的最后一行的行号

与 line(“.”) 区别

  • line("."): 会在所选范围的所有行上,每行会执行一次函数
  • range: 只会在第一行执行一次

示例

1
2
3
function! Breakfast() range
echo line(".")
endfunction
1
:11,20 call Breakfast

字典

没什么用处

也没怎么看懂

函数引用

类似于函数指针,将变量指向一个函数

使用

使用 function()

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function! Breakfast(item)
return "I am having " . a:item . " for breakfast"
endfunction

let Breakfastify = Breakfast
" returns error

let Breakfastify = function("Breakfast")

echo Breakfastify("oatmeal")
" returns "I am having oatmeal for breakfast"

echo Breakfastify("pancake")
" returns "I am having pancake for breakfast"

Lambda

未命名函数

示例

1
2
3
4
5
6
7
let Plus = {x,y -> x + y}
echo Plus(1,2)
" returns 3

let Tasty = { -> 'tasty'}
echo Tasty()
" returns "tasty"

You can call a function from insisde a lambda expression(没看懂)

方法链

用法

使用 ->

->跟在函数名后面,且中间不能添加空格

1
Source->Method1()->Method2()->...->MethodN()

说明

shell 的 ==管道==

Closure

1
2
3
4
5
6
7
8
9
function! Lunch()
let appetizer = "shrimp"

function! SecondLunch()
return appetizer
endfunction

return funcref("SecondLunch")
endfunction

appetizer在Lunch函数中定义,该函数返回SecondLunch函数。注意,SecondLunch使用了appetizer,但在Vimscript中,它没有访问该变量的权限。如果你尝试运行echo Lunch()(), Vim将抛出一个未定义变量错误。

解决

1
2
3
4
5
6
7
8
9
function! Lunch()
let appetizer = "shrimp"

function! SecondLunch() closure
return appetizer
endfunction

return funcref("SecondLunch")
endfunction

写插件