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-vim
和 nerdtree
,将下面的片段放到您的vimrc中:
1 | call plug#begin('~/.vim/plugged') |
然后保存修改,加载当前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 | let s:english_dict = "/usr/share/dict/words" |
在后面的章节中您将了解关于Vimscript赋值和条件的知识。
要查看Vim中所有可用的选项,查阅 :h E355
。
自定义函数
Vimrc是一个很好的用来放置自定义函数的地方。在后面的章节中您将学习如何写您自己的Vimscript函数。
自定义命令
您可以使用 command
创建一个自定义命令行命令。
比如,创建一个用于显示今天日期的基本命令 GimmeDate
:
1 | :command! GimmeDate echo call("strftime", ["%F"]) |
当您运行 :GimmeDate
时,Vim将显示一个类似 “2021-01-1”的日期。
要创建一个可以接收输入的基本命令,您可以使用 <args>
。如果您想向 GimmeDate
传递一个时间/日期格式参数:
1 | :command! GimmeDate echo call("strftime", [<args>]) |
如果您想限定参数的数目,您可以使用 -nargs
标志。-nargs=0
表示没有参数,-nargs=1
表示传递1个参数,-nargs=+
表示至少1个参数,-nargs=*
表示传递任意数量的参数,-nargs=?
表示传递0个或1个参数。如果您想传递n个参数,使用 -nargs=n
(这里 n
是一个任意整数)。
<args>
有两个变体:<f-args>
和 <q-args>
。前者用来向Vimscript函数传递参数,后者用来将用户输入自动转换为字符串。
使用 args
:
1 | :command! -nargs=1 Hello echo "Hello " . <args> |
使用 q-args
:
1 | :command! -nargs=1 Hello echo "Hello " . <q-args> |
使用 f-args
:
1 | :function! PrintHello(person1, person2) |
当您学了关于Vimscript函数的章节后,上面的函数将更有意义。
查阅 :h command
和 :args
了解更多关于command和args的信息。
键盘映射
如果您发现您重复地执行一些相同的复杂操作,那么为这些复杂操作建立一个键盘映射将会很有用:
比如,在我的vimrc文件中有2个键盘映射:
1 | nnoremap <silent> <C-f> :GFiles<CR> |
在第一个中,我将 Ctrl-F
映射到 fzf.vim 插件的 :Gfiles
命令(快速搜索Git文件)上。在第二个中,我将 <leader>tn
映射到调用一个自定义函数 ToggleNumber
(切换 norelativenumber
和 relativenumber
选项)。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 | source $HOME/.vim/settings/plugins.vim |
在 ~/.vim/settings/plugins.vim
里面:
1 | call plug#begin('~/.vim/plugged') |
在 ~/.vim/settings/configs.vim
里面:
1 | set nocompatible |
在 ~/.vim/settings/functions.vim
里面:
1 | function! ToggleNumber() |
在 ~/.vim/settings/mappings.vim
里面:
1 | inoremap jk <esc> |
这样您的vimrc文件依然能够正常工作,但现在它只有4行了。
使用这样的设置,您可以轻易知道到哪去修改配置。如果您要添加一些键盘映射,就将它们添加在 /mappings.vim
文件中。以后,当您的vimrc变大时,您总是可以新建几个子文件来缩小它的大小。比如,如果您想为主题配色创建相关设置,您可以添加 ~/.vim/settings/themes.vim
。
保持单独的一个Vimrc文件
如果您倾向于保持一个单独的vimrc文件,以使它更加便于携带,您可以使用标志折叠让它保持有序。在vimrc文件的顶部添加一下内容:
1 | " setup folds {{{ |
Vim能够检测当前buffer所属的文件类型 (:set filetype?
). 如果发现属于 vim
类型,您可以使用标志折叠。回想一个标志折叠的用法,它使用 {{{` 和 `}}}
来指明折叠的开始和结束。
添加 {{{` 和 `}}}
标志将您的vimrc文件其他部分折叠起来。(别忘了使用 "
对标志进行注释):
1 | " setup folds {{{ |
您的vimrc文件将会看起来类似下面:
1 | +-- 6 lines: setup folds ----- |
启动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 | if has('nvim') |
组织管理软件包
回想一下,要使用Vim的软件包系统必须有以下需求:
1 | ~/.vim/pack/*/start/ |
或者:
1 | ~/.vim/pack/*/opt/ |
实际上,*
星号可以使 任意 名字,这个名字就可以用来管理您的插件。假设您想将您的插件根据类型(颜色、语法、游戏)分组:
1 | ~/.vim/pack/colors/ |
您仍然可以使用各个目录下的 start/
和 opt/
。
1 | ~/.vim/pack/colors/start/ |
聪明地添加插件
您可能好奇,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.vim
和 chocolate.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时,事件BufNewFile
和 BufRead
就会被触发。 *.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 | if exists("did_load_filetypes") |
finish
是一个Vim命令,用来停止执行剩余的脚本。表达式"did_load_filetypes"
并 不是 一个Vim内置函数。它实际上是$VIMRUNTIME/filetype.vim
中的一个全局变量。如果您好奇,运行:e $VIMRUNTIME/filetype.vim
。您将会发现以下内容:
1 | if exists("did_load_filetypes") |
当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 | if did_filetype() |
函数 getline(1)
返回文件第一行的内容。它检查第一行是否以 “donutify” 开头。函数 did_filetype()
是Vim的内置函数,当一个与文件类型相关的事件发生至少一次时,它返回真。它用来做守卫,防止文件类型事件反复运行。
打开文件 sugardonut
然后运行 :set filetype?
,Vim现在返回 filetype=donut
。如果您打开另外一个donut文件 (glazeddonut
和 frieddonut
),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 | (donut "tasty") |
虽然Vim现在知道了正确的文件类型,但所有的文本都是相同的颜色。让我们添加语法高亮规则,使 “donut” 关键词高亮显示。创建一个新的 chocodonut 语法文件 ~/.vim/syntax/chocodonut.vim
,在文件中添加:
1 | syntax keyword donutKeyword donut |
现在重新打开 hello.chocodonut
文件,关键词 donut
已经高亮显示了。
本章不会详细介绍语法高亮。它是一个庞大的主题。如果您感兴趣,可以查阅 :h syntax.txt
。
vim-polyglot 插件非常的棒,它提供了很多流行的编程语言的语法高亮。
文档
如果您写了一个插件,您还得创建一个您自己的文档。您可以使用文档runtime路径完成这个。
让我们为 chocodonut 和 plaindonut 关键字创建一个基本文档。创建文件 donut.txt
(~/.vim/doc/donut.txt
)。在文件中,添加一下内容:
1 | *chocodonut* Delicious chocolate donut |
如果您试着搜索 chocodonut
或 plaindonut
(: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 | echo "tasty.vim global" |
注意,函数名是 tasty#donut
而不是 donut()
。要想使用autoload功能,井号(#
)是必须的。在使用autoload功能时,函数的命名惯例是:
1 | function fileName#functionName() |
在本例中,文件名是 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
类似于 python
的 print()
echom
: 和 echo 相同, 不同支出在于 echom
会将结果保存到 message 历史记录中
查看 message 历史记录
:messages
清空 mwssage 历史记录
:message clear
数字
模式指的是==十进制==数字
十进制 Decimal
1, -1, -10
十六进制 Hexadecimal
以 0x
或 oX
开头
二进制 Binary
以 0b
或 0B
开头
八进制 Octal
以 0
, 0o
或 0O
开头
真假值
- 真:非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 | :let sweeterList = ["glazed", "chocolate", "strawberry"] |
解包
像python
1 | :let favoriteFlavor = ["chocolate", "glazed", "plain"] |
使用 ;
分配剩余的元素
1 | :let favoriteFruits = ["apple", "banana", "lemon", "blueberry", "raspberry"] |
修改列表
修改一项
1 | :let favoriteFlavor = ["chocolate", "glazed", "plain"] |
修改多项
1 | :let favoriteFlavor = ["chocolate", "glazed", "plain"] |
字典
类似于 python
==区别==在于 vim 只使用字符串为“键”,即使使用数字,也会将数字转化为字符串
偷懒方法
使用 #{}
来表示
1 | :let mealPlans = #{breakfast: "waffles", lunch: "pancakes", 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 | :echo json_encode({"test": v:true}) |
False
1 | :echo json_encode({"test": v:false}) |
None
1 | :echo json_encode({"test": v:none}) |
被解释成 null
Null
1 | :echo json_encode({"test": v:null}) |
类似于 v:none
第26章 条件和循环
关系表达式
==
!=
>
>=
<
<=
注意:这里的字符串仍被转换为数字
字符串逻辑运算
用关系运算符来比较字符串
=~
:对字符串进行正则表达式匹配:可以模式匹配上!~
: 对字符串进行正则表达式匹配:不可以匹配上==
: 完全匹配:相等!=
:完全匹配:不相等
注意:如果在设置了
set ignorecase
,那么比较时也会忽略大小写
设置总是忽略大小写
在末尾加一个
#
,如:=~#
(推荐:安全)1
echo str =~# "hearty"
在末尾加一个
?
,如:=~?
1
echo str =~? "hearty"
if
最小的 if 语句
1 | if {clasuse} |
if 全貌
1 | if {predicate1} |
三元表达式
1 | {predicate} ? expressiontrue : expressionfalse |
补充
background
: 背景strftime()
: 目前时间
or
||
注意:短路运算
and
&&
注意:短路运算
for
方法1
1 | let breakfasts = ["pancakes", "waffles", "eggs"] |
方法2
1 | let meals = [["breakfast", "pancakes"], ["lunch", "fish"], ["dinner", "pasta"]] |
方法3
1 | let beverages = #{breakfast: "milk", lunch: "orange juice", dinner: "water"} |
while
常见
1 | let counter = 1 |
获得每行内容
1 | let current_line = line(".") |
Error Handling
break
和 python 一样:中断循环
Continue
和 python 一样:跳过当前轮
try, finally, and catch
和 python 一样
- finally: 总是被执行,且是最后被执行
- catch: 捕获错误
注
throw
:抛出一个错误
catch 捕捉的错误
:h :catch
1 | catch /^Vim:Interrupt$/. " catch interrupts (CTRL-C) |
第27章 变量范围
变量与常量
变量
定义变量
1 | let val = "value" |
let
定义的是==变量==
修改变量
当需要改变变量的值时,需要使用 let
1 | let val = "value" |
使用变量
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 | function {FunctionNmae}() |
注意:
- ==函数名必须以大写字母开头==
- 函数名不能以数字开头
函数名小写字母开始
使用脚本变量前缀 s:
,如:function s:tasty()
同名函数
不允许有重名函数
覆写函数
在 function
后面使用 !
1 | function! Tasty() |
覆写之后上面的函数就会没有。==覆写不是重载==哦
查看所有可用函数
使用 :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 | function! Tasty(food) |
局部变量
用法
使用 l:
常见问题
问题 1
若在函数内部定义一个变量,那么这个变量默认为局部变量
1 | function! Tasty() |
与下面代码等价,但是==没有下面的代码好==
1 | function! Tasty() |
问题 2
1 | function! Calories() |
变量会与特殊变量同名,导致错误
错误根源: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 | function! Breakfast(meal, beverage = "Milk") |
可变参数
使用 ...
示例
1 | function! Buffet(...) |
a:0
: 共有多少个参数a:1
是第一个参数a:2
是第二个参数- …
a:20
是第20个参数a:000
是所有参数值组成的列表
a:0 示例
1 | function! Buffet(...) |
a:000 + for
1 | function! Buffet(...) |
Range
在定义函数的第一行最后加入 range
效果
会给该函数添加两个特定变量:
a:firstline
: 所选范围的第一行的行号b:lastline
: 所选范围的最后一行的行号
与 line(“.”) 区别
line(".")
: 会在所选范围的所有行上,每行会执行一次函数range
: 只会在第一行执行一次
示例
1 | function! Breakfast() range |
1 | :11,20 call Breakfast |
字典
没什么用处
也没怎么看懂
函数引用
类似于函数指针,将变量指向一个函数
使用
使用 function()
示例
1 | function! Breakfast(item) |
Lambda
未命名函数
示例
1 | let Plus = {x,y -> x + y} |
You can call a function from insisde a lambda expression(没看懂)
方法链
用法
使用 ->
->
跟在函数名后面,且中间不能添加空格
1 | Source->Method1()->Method2()->...->MethodN() |
说明
像 shell
的 ==管道==
Closure
1 | function! Lunch() |
appetizer在Lunch函数中定义,该函数返回SecondLunch函数。注意,SecondLunch使用了appetizer,但在Vimscript中,它没有访问该变量的权限。如果你尝试运行echo Lunch()()
, Vim将抛出一个未定义变量错误。
解决
1 | function! Lunch() |
写插件
略