一位老Vimer的插件配置和开箱即用的Vim镜像

  1. Vim
    1. Vim基础
    2. Vim基本配置
      1. Tab
      2. 自动缩排
  2. Vim插件管理器
    1. Vundle.vim
    2. vim-plug
    3. Vundle.vim和vim-plug对比
  3. Vim常用插件
    1. bash-support
    2. NERDtree
    3. bufexplorer
    4. LeaderF
    5. Gutentags
    6. Tagbar
    7. Fugitive
    8. a.vim
    9. vim-lua-format
    10. vim-go
    11. YouCompelteMe
      1. 安装说明
      2. 安装问题记录
      3. 配置说明
  4. 开箱即用的Vim镜像
    1. 镜像构建中的问题

10年Vimer准备转战Visual Studio Code了,这些年的Vim使用来看,和VS Code这种IDE对比下来,稳定性和高效都相对差一点,特别是近两年,频繁的在C++,Go,Lua,Python之间来回切换,能明显感觉Vim的稳定性和效率都会明显下降。不过还是总结一下Vim的使用,和一些插件,以及NeoVim,Visual Studio Code的相关配置。

Vim

为什么会走上使用vim之路,可能刚接触Linux那会(实验室的机房),扑面而来黑洞洞的bash shell,只知道能用vim来进行文件的操作,后来接触到vim的指令,以及vim的丰富的插件生态,不禁感叹vim那么强大,想着随便一个终端,连上开发服务器都可以快速的进行Coding,这可能也是让我坚持到现在的原因吧。

随着Iaas基础设施和Cloud Native的的发展,稳定,高性能的远端开发容器( or VM)+轻终端的开发模式成为了越来越多人的首选,Visual Studio Code等功能强大的IDE也能够支持连接远端服务器进行远程开发。基于docker image的不可变性,我们可以将我们的配置和插件全部镜像化,这样就可以在任何地方使用了。

Vim基础

Vim环境的配置里有两个很重要的文件~/.viminfo~/.vimrc

.viminfo是记录vim动作的文档,也就是通过vim所进行的编辑操作,都会在该文档有所记录,这就是为什么重复编辑同一个文档时,第二次进入该文档,光标会停留在上次离开的位置。当然该文档还会标记对操作的每个文件的编辑历史及其动作。

.vimrc是当前用户vim环境的配置文件。系统的vim环境配置文件为/etc/vim/vimrc,一般不建议修改。.vimrc文件默认情况下不会存在,需要用户自己创建。Vim的环境设定参数有很多,下面是一些基本常用的参数:

VIM命令 备注
:set nu
:set nonu
显示/隐藏行号
:set hlsearch
:set nohlsearch
high light search针对搜索结果进行高亮
:set autoindent
:set noautoindent
自动缩排
:set backup 自动备份文档,当更新一个档案时,
源文件会被另存成一个名为 filename~的文件
:set ruler 右下角显示状态栏
:set backspace=(0|1|2) 在INSERT模式下,默认可以删除任意字符,但此功能可以修改
backspace=2:可以删除任意字符
backspace=0|1:仅仅可以删除在此次进入INSERT模式下输入的字符
:syntax on
:syntax off
根据程序类型是否显示语法相关不同颜色
:set bg=dark
:set bg=light
设置背景色

这里需要注意的是,Windows下vim的个人配置文件在C:\用户\username\下对应的用户名下面。需要注意的是Linux用户目录下的vim配置文件与Windows下的差异如下。

系统 用户vim环境配置 用户vim配置目录
Windows .vimrc .vim
Linux _vimrc vimfiles

这两个文件默认也同样是没有的,需要自己创建。

Vim基本配置

在每次第一次使用vim时候,都需要对vim的基本环境进行自己习惯性的设置,特别是在通过vim进行代码编写的时候,还需要进行插件的安装。后面在介绍bash-support的时候也会提及,bash-support的插件中提供了一个标准化的.vimrc,可以直接作为我们的基础.vimrc来使用,我一直就是基于这个.vimrc来进行定制化配置的。

下面两个基本配置是以前记录的,后面详细完善一下吧。可以先不用看

Tab

在代码编写过程中,众所周知,各个软件对tab的宽度设置不一,然后有时候又会结合空格来写代码,导致代码夹杂中tab和空格,惨不忍睹,所以很多时候我们都建议在使用任何编辑器的时候,都要将tab的宽度设置为4个字符,且转换为空白符插入:

TAB替换为空格:

1
2
3
:set ts=4
:set expandtab
:%retab!

空格替换为TAB:

1
2
3
:set ts=4
:set noexpandtab
:%retab!

!是用于处理非空白字符之后的TAB,即所有的TAB,若不加!,则只处理行首的TAB。我常用的配置是

1
2
set ts=4
set expandtab

自动缩排

filetype indent on这个命令,首先,它会检查文件的类型。这与设置语法高亮时所做的类型检查是一样的。一旦Vim确定了文件类型,它就会为此类型的文件搜索一个对应的定义其缩进风格的文件。 Vim的发布版中包含了很多为每种语言设置不同缩进风格的脚本文件(/usr/share/vim/vimfiles/indent)。这些脚本文件正是为你在编辑中使用自动缩进功能进行准备工作。如果你不想用自动缩进的话,还可以再把它关闭掉:filetype indent off

使用自动缩进最简单的形式是打开autoindent选项。它使用当前行前面一行的缩进。

1
2
filetype indent on
:set autoindent shiftwidth=4

Vim插件管理器

Vim的生态相对比较丰富,有着各式各样的插件可供使用。虽然Vim从6.0开始内置支持了Vimball:一种标准化的插件打包和发布各式,让插件的安装和管理更加简单方便。但是Vim本身并不提供自动化的插件的安装,管理,分发,也就是Vim并没有直接提供Vim生态的registry软件源的服务。所以Vimball逐渐被流行的插件管理器所取代,例如:Vundle.vimvim-plug

现在介绍一下和对比一下我接触过比较流行的两个Vim插件管理器,Vundle.vimvim-plug,下面简单介绍一下:

Vundle.vim

Vundle.vim本身作为Vim的一款插件,用来管理Vim的其他插件。Vim本身有一款自己的插件管理器vimball,该管理器安装插件还是要针对每一个插件,都需要进行:下载,手动安装。而vundle安装插件只需要在vimrc中添加一行就搞定了,Vundle支持的功能有以下几点:

  • 通过.vimrc来跟踪和配置管理插件;
  • 安装配置的插件;
  • 更新配置的插件;
  • 搜索指定的插件;
  • 删除不使用的插件

Vundle的安装过程,以及插件的安装过程其实很简单,在github的README上都有,简述如下:

1
git clone https://github.com/gmarik/Vundle.vim.git  ~/.vim/bundle/Vundle.vim

然后就可以在.vimrc进行配置需要依赖的插件了,这里只罗列一下Vundle支持安装的插件类型,如下:

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

" 将vundle插件添加到运行时环境路径
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

"==========================插件存放起始============================

" 让vundle来管理vundle, required
Plugin 'VundleVim/Vundle.vim'

" 下面是vundle支持的插件格式和相关功能

" 1. GitHub中的vim插件
Plugin 'tpope/vim-fugitive'

" 2. 来自 http://vim-scripts.org/vim/scripts.html的插件
Plugin 'L9'

" 3. Git管理的插件,但不在GitHub中
Plugin 'git://git.wincent.com/command-t.git'

" 4. git管理的插件,在本地 (i.e. when working on your own plugin)
Plugin 'file:///home/gmarik/path/to/plugin'

" 5. 插件位于github仓库的vim目录下
Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}

" 6. 避免和已经安装的L9插件重名,将该github的插件重命名为newL9
Plugin 'user/L9', {'name': 'newL9'}

"==========================插件存放终止============================

"所有的插件都必须放在begin()和end()之间

call vundle#end()

.vimrc中配置好自己需要的插件后,进入vim,在命令模式下输入PluginInstall就可以安装未安装的插件,够方便吧。Vundle支持一下命令:

1
2
3
4
5
:PluginList    //list已安装的插件 
:PluginInstall //安装插件, append `!` 表示更新插件
:PluginUpdate //更新插件,等同于PluginInstall !
:PluginSearch foo//搜索插件
:PluginClean //删除插件

vim-plug

vim-plug是一款极简主义的Vim插件管理器,它的特点是:

  • 安装简单:只需要一个文件,不需要引用任何代码;
  • 使用简单:简明直观的语法;
  • 超级快的插件安装和更新:依赖于并行操作;
  • 浅克隆,减少磁盘空间和下载时间;
  • 启动时按需加载,以加速vim启动;
  • 可以检查和回滚更新;
  • 可以支持指定插件的:Branch/tag/commit;
  • 可以支持插件下载后执行:Post-update hooks
  • 支持外部工具管理插件;

安装vim-plug插件管理器很方便,如下:

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

和Vundle一样,然后就可以在.vimrc进行配置需要依赖的插件了,这里罗列一下vim-plug支持安装的插件类型,如下:

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
36
37
38
39
40
41
42
43
44
45
call plug#begin()
" The default plugin directory will be as follows:
" - Vim (Linux/macOS): '~/.vim/plugged'
" - Vim (Windows): '~/vimfiles/plugged'
" - Neovim (Linux/macOS/Windows): stdpath('data') . '/plugged'
" You can specify a custom plugin directory by passing it as the argument
" - e.g. `call plug#begin('~/.vim/plugged')`
" - Avoid using standard Vim directory names like 'plugin'

" Make sure you use single quotes

" 1. 安装来自GitHub的插件; translates to https://github.com/junegunn/vim-easy-align
Plug 'junegunn/vim-easy-align'

" 2. 安装来自任意git URL的插件
Plug 'https://github.com/junegunn/seoul256.vim.git'

" 3. 安装来自github的插件,且是release的tag版本; (通配符requires git 1.9.2 or above)
Plug 'fatih/vim-go', { 'tag': '*' }

" 4. 安装来自github的插件,指定branch
Plug 'neoclide/coc.nvim', { 'branch': 'release' }

" 5. 安装来自github的插件,指定插件安装的目录
Plug 'junegunn/fzf', { 'dir': '~/.fzf' }

" 6. 安装来自github的插件,指定插件安装的目录,并设置Post-update hook
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }

" 7. 安装来自github的插件,Post-update hook can be a lambda expression
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }

" 8. 安装来自github的插件,且支持插件位于repo的指定目录
Plug 'nsf/gocode', { 'rtp': 'vim' }

" 9. 按需加载设置:在执行指定命令后才加载插件
Plug 'preservim/nerdtree', { 'on': 'NERDTreeToggle' }

" 10. 按需加载设置:在打开特定类型文件后才加载插件
Plug 'tpope/vim-fireplace', { 'for': 'clojure' }

" 11. 手动管理的插件
Plug '~/my-prototype-plugin'

call plug#end()

vim-plug支持的更详细的命令和参数选项可以参考:README

Vundle.vim和vim-plug对比

上面简单介绍了一下Vundle.vim和vim-plug,可以看到vim-plug支持的功能相对比较丰富:

  • vim-plug可以指定插件的branch/tag/commit,而Vundle.vim不支持;
  • vim-plug可以支持插件安装后,设置Post-update hooks,而Vundle.vim不支持;
  • vim-plug可以支持插件按需加载,而Vundle.vim不支持;
  • vim-plug可以支持插件安装时进行浅克隆,以加速安装和节省磁盘空间,而Vundle.vim不支持;

而除了Vundle.vim支持非git repo插件的安装外,其他所有功能vim-plug都支持,对比vim-plug的stars:33K,而Vundle.vim的stars:23.8k,且感觉Vundle.vim已经很不活跃了,且GitHub官方社区有个关于[VundleVim Org 被封,导致VundleVim仓库消失](The VundleVim organization has been flagged.)的讨论,综合考虑,我还是建议使用vim-plug插件来管理vim的插件;

Vim常用插件

Vim官网的Scripts目录(http://www.vim.org/scripts/)罗列了很多比较优秀的Vim插件。这个Scripts目录最初是作为一个用户贡献的Vim脚本存档而创建的,但随着时间推移,越来越多的插件开发者选择使用诸如GitHub之类的代码托管平台来发布和维护他们的插件。且可以发现Vim官网的Scripts目录的插件基本更新日志都停留在很久之前,所以参考意义有待商榷。

不过官网的Scripts列表可以进行按照评分排序,排名靠前的插件还是很有用的。

bash-support

bash-support是一款为Vim 提供 Bash 脚本语言支持的插件。它为 Bash 脚本文件带来了代码片段插入,语法高亮、代码折叠、关键字补全,脚本运行,调试等基本功能,旨在提升 Vim 中编写 Bash 脚本的体验。

我觉得主要功能有(参考bash-support帮助文档 2. USAGE without GUI (Vim)):

  • 创建shell文件的时候,自动添加文件注释头;
  • 快速的注释管理:行尾注释,端注释,函数注释等;
  • 快速的插入声明语句:case,if,function等语句;

还有一个很赞的就是,bash-support的插件中提供了一个标准化的.vimrc,可以直接作为我们的基础.vimrc来使用,我一直就是基于这个.vimrc来进行定制化配置的。

安装要求:Vim version 7.4+ or Neovim 0.2.1+;

基于vim-plug安装如下:

1
Plug 'WolfgangMehner/bash-support', { 'commit': '99c746c' }

安装后,简单的演示图如下:

常用的命令如下:

1
2
3
4
5
6
7
\sc     case in ... esac                    (n, i)
\sf for in do done (n, i, v)
\sie if then else fi (n, i, v)
\sw while do done (n, i, v)
\sfu function (n, i, v)
\cfr frame comment
\cfu function description

NERDtree

NERDTree是一款功能强大的Vim文件系统浏览插件,主要的功能就是以树状结构浏览整个文件系统,并可以快速的打开文件和目录。直接通过vim-plug插件安装:

1
Plug 'preservim/nerdtree', { 'tag': '7.1.2' }

NERDTree的全局指令主要有以下几个,不过我一般只用快捷指令映射的:NERDTree,:

  • :NERDTree [<start-directory> | <bookmark>]:打开指定的目录树,如果不传参数,默认打开当前工作目录,否则打开对应的目录,或者书签指定的目录(后面不再提书签,因为感觉很少用到)
  • :NERDTreeVCS [<start-directory> | <bookmark>]:和:NERDTree类似,但是会从当前工作目录向上搜索repo版本控制所在的目录,并将其设置为root。支持的VCS包含:Git,Subversion,Mercurial,Bazaar,Darcs;
  • :NERDTreeToggle [<start-directory> | <bookmark>]:如果 NERDTree 窗口已打开,则关闭它,否则打开它;
  • :NERDTreeRefreshRoot:刷新当前NERDTree 根节点的目录树;
  • :NERDTreeCWD:切换NERDTree 根节点到当前的工作目录;

NERDTree还有针对目录树的Bookmark命令,主要是用来针对目录创建书签,方便快速跳转,但是实际很少使用,有兴趣的可以查看NERDTree的帮助文档。

除了上面提供的全局命令,在NERDTree的窗口中,还提供了很多快捷映射键,下面只列出我比较常用的,具体可查看NERDTree的帮助文档(:h NERDTree):

1
2
3
4
5
6
7
8
9
10
11
12
o: 展开选择的目录,打开选择的文件
O: 递归展开当前选择的目录的所有子目录
X: 递归收起当前目录的所有子目录
u: 返回上层父目录,将上层目录设置为目录根
C: 进入当前目录,将当前目录设置为目录根
go: 打开选择的文件,但是光标保持在NERDTree窗口
i: 打开选择的文件,和o的区别是,以split新窗口的方式打开
P: 跳转到当前目录树的root node
p: 跳转到当前目录树的节点的父目录
cd: 将当前选择的node作为CWD
CD: 切换当前的目录树的root到CWD
q: 关闭NERDTree目录树

下面是我的.vimrc关于NERDTree的快捷键映射:

1
2
3
4
" 将:NERDTree命令映射到快捷键:\NE
nnoremap <script> <silent> <unique> <Leader>NE :NERDTree%<CR>
" NERDTree显示文件的行数
let g:NERDTreeFileLines = 1

NERDTree的README中有更多实用的配置,这里不做介绍,其中有一个vim打开文件自动启动NERDTree的功能,但是我测试发现,会导致内容串行,所以就没有启用。还有一个不好的就是NERDTree没有文件递归查找的功能。

下面是NERDTree安装后的使用示意:

bufexplorer

bufexplorer插件,主要用来查看和打开已编辑过的历史文件列表,以达到快速切换文件的目的。

基于vim-plug进行安装如下:

1
Plug 'jlanzarotta/bufexplorer', { 'commit': '20f0440' }

安装完成后,bufexplorer的使用很方便,bufexplorer提供了内置了的快捷键映射,包括:

1
2
3
4
\be: 正常打开bufexplorer
\bt: 打开或关闭bufexplorer窗口
\bs: 水平分割窗口打开bufexplorer
\bv: 垂直分割窗口打开bufexplorer

当然可以通过:BufExplorer命令来进行操作,如下是使用界面:

LeaderF

LeaderF是一款很强大的模糊查找工具,能够快速的定位文件,缓冲区,最近使用文件,标签等

LeaderF的安装要求:

  • vim 7.4.1126+;
  • Python2.7+ or Python3.1+;
  • popup mode, neovim 0.5.0+ or vim 8.1.1615+ are required.

基于vim-plug进行安装如下:

1
Plug 'Yggdroot/LeaderF', { 'tag': 'v1.25', 'do': ':LeaderfInstallCExtension' }

为了更好的搜索性能,LeaderF需要安装C扩展(见上面插件安装过程),基于这个扩展,模糊搜索的速度可以快10倍以上。安装成功后,可以在vim中通过echo g:Lf_fuzzyEngine_C来查看,如果输出1表示安装成功。

LeaderF支持子命令主要包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
file                在工作空间中搜索文件,可以定位和预览匹配的文件
tag 在tags文件中搜索关键字,可以定位和预览匹配的文件及其关键字
function 在当前窗口buffer中搜索函数,感觉用处不大,还不如直接/xxx来搜索
mru 在最近打开的文件中搜索文件,应该是通过~/viminfo历史来实现的
searchHistory 搜索之前的搜索历史,并执行
cmdHistory 搜索vim历史命令,并执行
help navigate the help tags
line 在当前窗口buffer中按行搜索,主要用途还是罗列,方便预览,搜索函数
colorscheme switch between colorschemes
gtags 使用gtags生成的符号索引进行搜索
self 列出所有Leaderf命令,选择执行
bufTag 在当前窗口buffer中搜索tag
buffer 在当前窗口buffer中搜索文件,用处不大,我用的bufexplorer插件管理打开文件列表
rg 通过rg来进行正则搜索
filetype navigate the filetype
command execute built-in/user-defined Ex commands.
window search windows.
quickfix navigate the quickfix.
loclist navigate the location list.

Leaderf的README给出了建议的配置,我在其上面,稍作修改,如下:

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
36
37
38
let g:Lf_CacheDirectory = expand('~/.vim/cache')

" 优先级A>F, 参考帮助文档
" A: 表示把当前文件的最近祖先(g:Lf_RootMarkers定义的)作为工作目录,
" F: 如果当前工作目录不是当前文件的直接祖先节点,则使用当前文件所在目录作为工作目录
let g:Lf_WorkingDirectoryMode = 'AF'

" don't show the help in normal mode
let g:Lf_HideHelp = 1
let g:Lf_UseCache = 0
let g:Lf_UseVersionControlTool = 0
let g:Lf_IgnoreCurrentBufferName = 1
" popup mode
let g:Lf_WindowPosition = 'popup'
let g:Lf_StlSeparator = { 'left': "\ue0b0", 'right': "\ue0b2", 'font': "DejaVu Sans Mono for Powerline" }
let g:Lf_PreviewResult = {'Function': 0, 'BufTag': 0 }

let g:Lf_ShortcutF = "<leader>lff"
noremap <leader>lfb :<C-U><C-R>=printf("Leaderf buffer %s", "")<CR><CR>
noremap <leader>lfm :<C-U><C-R>=printf("Leaderf mru %s", "")<CR><CR>
noremap <leader>lfbt :<C-U><C-R>=printf("Leaderf bufTag %s", "")<CR><CR>
noremap <leader>lft :<C-U><C-R>=printf("Leaderf tag %s", "")<CR><CR>
noremap <leader>lfl :<C-U><C-R>=printf("Leaderf line %s", "")<CR><CR>

"noremap <C-B> :<C-U><C-R>=printf("Leaderf! rg --current-buffer -e %s ", expand("<cword>"))<CR>
"noremap <C-F> :<C-U><C-R>=printf("Leaderf! rg -e %s ", expand("<cword>"))<CR>
" search visually selected text literally
xnoremap gf :<C-U><C-R>=printf("Leaderf! rg -F -e %s ", leaderf#Rg#visual())<CR>
noremap go :<C-U>Leaderf! rg --recall<CR>

" should use `Leaderf gtags --update` first
let g:Lf_GtagsAutoGenerate = 0
let g:Lf_Gtagslabel = 'native-pygments'
noremap <leader>lfgr :<C-U><C-R>=printf("Leaderf! gtags -r %s --auto-jump", expand("<cword>"))<CR><CR>
noremap <leader>lfgd :<C-U><C-R>=printf("Leaderf! gtags -d %s --auto-jump", expand("<cword>"))<CR><CR>
noremap <leader>lfgo :<C-U><C-R>=printf("Leaderf! gtags --recall %s", "")<CR><CR>
noremap <leader>lfgn :<C-U><C-R>=printf("Leaderf gtags --next %s", "")<CR><CR>
noremap <leader>lfgp :<C-U><C-R>=printf("Leaderf gtags --previous %s", "")<CR><CR>

针对Leaderf我觉得比较常用的是:filetaglinemru,下面是相关演示:

Gutentags

GutentagsVim插件用于管理Vim中非常需要的tags标签文件。它会在你工作时自动(重新)生成标签文件,而且完全不会影响到你。它甚至会尽最大努力将这些标签文件也不会妨碍到你。它没有任何依赖,只需安装就可以正常工作。

和其他大部分Vim插件一样,为了生成tags文件,Gutentags需要先弄清楚你的项目空间,为此需要它需要知道项目的根目录标记,例如版本控制的文件夹.git.svn.hg等,这个是由环境变量let g:gutentags_project_root来控制的,不过默认该值为空(这个设计不好,按道理应该配置上常规的root marker),所以需要我们自己安装后进行配置。

当你编辑的文件被发现位于上面标记的项目中,Gutentags将确保该项目的标签文件是最新的。之后,当你在该项目的文件中工作时,它会部分重新生成标签文件。每次保存时,它会在后台悄悄更新该文件的标签。

Gutentags很大的特性就是tags文件增量修改。Gutentags是基于ctags来生成和追加新的tags,由于ctags不支持删除,所以在文件发送变化时,Gutentags会从tags文件中删除不存在的tags,然后再通过ctags进行tags的添加,这样就可以保持tags文件和源码文件的一致性。

Gutentags足够智能,不会在你频繁保存文件或项目非常庞大时触发多个ctags进程,从而互相干扰。

Gutentags不会触动手动设置的tags变量,它只会将自动生成的tags文件追加到tags列表中。这样可以避免与用户手动配置的tags文件冲突。且这样其他基于tags文件的插件,也能自动使用到最新和源码一致的tags,例如LeaderF的模糊搜索也是基于tags文件。

通过vim-plug进行安装如下:

1
Plug 'ludovicchabant/vim-gutentags', { 'commit': 'aa47c5e' }

安装完成后,由于Gutentags的默认配置很多为空,所以需要简单配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
" gutentags搜索工程目录的标志,碰到这些文件/目录名就停止向上一级目录递归,默认为空"
let g:gutentags_project_root = ['.root', '.svn', '.git', '.project']

" 所生成的tags文件的名称,默认为tags"
let g:gutentags_ctags_tagfile = '.tags'

" 将自动生成的 tags 文件全部放入 ~/.cache/tags 目录中,避免污染工程目录 "
let s:vim_tags = expand('~/.cache/tags')
let g:gutentags_cache_dir = s:vim_tags
" 检测 ~/.cache/tags 不存在就新建 "
if !isdirectory(s:vim_tags)
silent! call mkdir(s:vim_tags, 'p')
endif

" 配置 ctags 的参数 "
let g:gutentags_ctags_extra_args = ['--fields=+niazS', '--extra=+q']
let g:gutentags_ctags_extra_args += ['--c++-kinds=+pxI']
let g:gutentags_ctags_extra_args += ['--c-kinds=+px']

安装完成后,打开项目工程文件,Gutentags就会自定生成项目的tags文件,并将其添加到tags变量中,以供基于tags文件的其他Vim插件使用,如下:

1
tags=~/.cache/tags/data-vgserver_proj-server-.tags,./tags,./TAGS,tags,TAGS 

Tagbar

Tagbar插件提供了一种简单的方式来浏览当前文件的标签,并获取其结构的概览。它通过创建一个侧边栏来实现这一点,该侧边栏按作用域显示当前文件的由ctags生成的标签。这意味着例如在C++中,方法会显示在它们所定义的类下面。

Tagbar不是一个管理tags文件的工具,它只是根据已有的tags文件,在内存中创建tags结构,以供结构的概览。

Tagbar安装依赖:

  • Vim >= 7.3.1058 or 任意版本的 NeoVim.
  • ctags:Tarbar强烈建议使用Universal Ctags,它是从Exuberant Ctags fork出来的项目,E-Ctags目前已经不维护了,U-Ctags更新了其很多bug和支持了更多的格式,以及unicode支持。

通过vim-plug插件安装如下:

1
Plug 'preservim/tagbar', { 'commit': '12edcb5' }

安装完成后,执行:Tagbar就可以打开当前文件的结构概览了,由于比较简单就没必要配置什么快捷键了,下面我单独配置了Vim启动,自动开启Tagbar的配置:

1
2
" 打开vim自动开启
autocmd VimEnter * Tagbar

如下是Tagbar的安装后使用效果:

其实在使用Tagbar之前还有一个历史比较悠久的taglist,在vim-scripts官网上下载量排第一,但是taglist已经基本不维护了,而且Tagbar相对taglist结构展示更加结构化,美观,且可折叠展开,如下:

Fugitive

vim-fugitive一款优秀的Git的Vim插件,它可以通过调用:Git指令加上任意git的命令,就可以完成git一样的操作,和使用git 命令行的效果一样,不过做了一些改进,对于我来说fugitive最常用的就是可以查看任何打开文件的每行的体积历史。

通过vim-plug安装如下:

1
Plug 'tpope/vim-fugitive', { 'commit': '2377e16' }

安装完成后,我配置了两个快捷键:

1
nnoremap <script> <silent> <unique> <Leader>gitb :Git blame<CR>                                   nnoremap <script> <silent> <unique> <Leader>gitd :Git diff<CR> 

如下是通过:Git blame命令查看文件blame的提交信息,这个是目前用的最多的,其他的git操作,其实都是在shell下操作了。

a.vim

a.vim是一个单纯用来快速切换C/C++的头文件和源文件的插件,还是比较好用的,不过已经不在维护了,最后一个版本还是停留在2007年,不过够用就行。

通过vim-plug安装如下:

1
Plug 'vim-scripts/a.vim', { 'tag': '2.18' }

a.vim使用方式也很简单,支持命令:

1
2
3
:A, 最常用的头文件和源文件切换
:AS, 切割窗口并进行切换,默认上下切分
:AV, 垂直切割窗口并进行切换,默认上下切分

安装后使用效果如下:

vim-lua-format

vim-lua-format是一款基于LuaFormatter的Lua代码Format的Vim插件,使用起来比较简单。

基于vim-plug安装:

1
Plug 'andrejlevkovitch/vim-lua-format', { 'commit': '9996af0' }

安装后配置相关快捷键和命令:

1
2
3
4
5
"快捷键方式<ctrl+c ctrl+k>
nnoremap <buffer> <c-k> :call LuaFormat()<cr>

"命令模式下直接输入LuaFormat进行格式化,借鉴ClangForamt
:command LuaFormat call LuaFormat()

vim-go

vim-go是一款提供Vim Go语言开发的插件,其实后面解释的YCM也支持Go语言的Semantic级别的补全,但是其他方面例如导航,错误检查,格式化等支持性和便捷性相对有一些差异,所以针对Go语言开发,vim-go插件还是很有用的。

vim-go主要支持的功能包括:

  • 支持Go的:构建、运行、测试:GoBuild 编译包,:GoInstall 安装包,:GoTest 运行测试,:GoTestFunc 运行单个测试函数,:GoRun 快速执行当前文件
  • 语法高亮、折叠与格式化
  • 调试支持:通过 :GoDebugStartdelve 集成调试;
  • 代码补全与语言服务:通过 gopls 提供补全和其他语言服务支持,和YCM比很差;
  • 代码导航:GoDef 跳转到符号/声明的位置,:GoDoc/:GoDocBrowser 查看文档;
  • 包管理:GoImport 导入包,:GoDrop 删除导入的包;
  • 静态分析:GoLint/:GoMetaLinter lint代码,:GoVet 捕获静态错误,:GoErrCheck 检查错误处理;
  • 高级源代码分析:GoImplements/:GoCallees/:GoReferrers 等通过 gopls 实现;

基于vim-plug安装:

1
Plug 'fatih/vim-go', { 'commit': '14eedf6', 'do': ':GoUpdateBinaries' }

安装完成后,基本可以做到开箱即用,无需什么配置。

YouCompelteMe

YouCompleteMe,一款Vim的代码补全引擎,它支持:

  • 快速的,即时输入的代码补全功能。
  • 支持模糊搜索代码补全,即所谓的identifier completer,基于标识符的代码补全。
  • 支持代码理解,即理解代码上下文语义,提供更准确的建议,即所谓的 semantic completer,基于语义的代码补全。
  • 支持代码重构,即根据上下文语义对代码进行修改、优化等操作。

其实简单来说也YCM的补全引擎包含两类:

  • identifier completer标识符补全器,即输入任意字符而弹出的浮动窗口,基于标识符补全出现的匹配字符串后面有一个[ID]
  • semantic completer语义补全器,即在 C++ 中输入 . 或 -> 后弹出的浮动窗口。

由于公司开发环境限制了Linux的版本,导致直到最近我才基于Docker Image真正配置上YouCompleteMe插件进行开发体验,果然是真香,感觉有了媲美IDE的Semantic Completion的功能。

YCM拥有一些内置的补全引擎,且支持任何符合LSP协议规范的Language Server(目前主流语言都有相对成熟的Language Server),因此可以为任何语言提供Semantic级别的代码补全。下面是一些主流语言支持LSP的Language Server列表:

1
2
3
4
5
6
7
8
9
1. C/C++/Objective-C (clangd)
2. Go (gopls)
3. Rust (rust-analyzer)
4. Python (pylsp)
5. Java (Eclipse JDT Language Server)
6. JavaScript/TypeScript (typescript-language-server)
7. PHP (intelephense)
8. Ruby (solargraph)
9. Bash (bash-language-server)

YCM内置包含的:

  • 一个基于字符匹配的引擎,identifier completer,支持任何语言使用,就是所谓的模式搜索代码补全;
  • 一个基于clangd的强大引擎,可以为C/C++/Objective-C/Objective-C++/CUDA 提供原生的 semantic completion。但是这里 需要注意:YCM版本内置的clangd版本编译依赖的是特定的Linux发行版(2024-03目前依赖的是Ubuntu 22.04),所以在其他发行版本不一定可以使用,因为编译依赖的glibc版本不一样。所以如果遇到这个问题,编译和配置的时候使用自定义的clangd引擎就好。
  • 一个基于Jedi的自动补全引擎,用来支持Python2/Python3的 semantic completion
  • 一个基于OmniSharp-Roslyn的自动补全引擎,用于支持C#的 semantic completion
  • 一个基于Gopls自动补全引擎,用于支持Go的 semantic completion
  • 一个基于TSServer自动补全引擎,用于支持JavaScript和TypeScript的 semantic completion
  • 一个基于rust-analyzer自动补全引擎,用于支持rust的 semantic completion
  • 一个基于jdt.ls自动补全引擎,用于支持rust的 semantic completion
  • 一个为了任何语言所设计的通用的LSP实现。
  • 一个基于omnifuc的自动补全引擎,它利用Vim的omnicomplete系统提供的数据,为许多其他语言(如Ruby、PHP等)提供语义补全功能。

如下是我的配置正常的YCM,在vim编辑代码时的自动补全界面:

在上面演示中,我们无需按任何快捷键就能够获取代码补全的候选列表,我们只需要正常输入代码,建议就会自动弹出,至于是否使用补全,取决于我们的选择。如果自动提示补全的代码是我们要的,按TAB键就可以将其输入,否则我们继续输入,忽略补全提示就好。

这里需要注意:自动补全的提示并不是基于输入串的前缀匹配,而是基于输入串的子序列匹配,即任何输入的字符都需要是按顺序的存在于目标字符串中,例如abcxaybgc 的子序列,但不是 xbyxaxxc 的子序列。过滤之后,一个复杂的排序系统会将最相关的补全字符串排在菜单的顶部,因此一般只需要按TAB键一次。

上面演示的是基于标识符的代码补全引擎identifier completer,我们看到自动补全的字符串后面有一个[ID],即表示是由identifier completer完成的。该功能适用于任何编程语言。标识符的匹配是来源你打开的当前的文件,你访问过的其他文件,以及你的tags文件中的所有标识符。

下面的演示动画中,是YCM的基于 semantic completer语义引擎的自动补全。在输入 .->:: (对于 C++,其他语言使用不同的触发器),语义引擎就会被触发。

上面这个演示中也可以看到YCM一个很重要的特性:语法诊断显示功能(左侧有>>高亮,可配置外显),该功能受 Syntastic启发(README已经停止维护,建议使用ALE,但是我觉得YCM更强大)。语法诊断显示也是自动完成的,不需要额外的快捷键或者保存文件来触发。

YCM还号称可能是唯一一个Vim自动补全引擎支持Unicode补全的,但是话说我们也没机会用到吧,谁没事输入unicode字符呀,这一般需要虚拟键盘才可以输入。

YCM针对一些编程语言提供了类似IDE的语义特性:

如下展示了查找标识符声明,引用,显示函数说明的简单功能:

安装说明

由于YCM一直以来的策略是支持最新的Ubuntu LTS的最新vim版本,且依赖的GLIBC的都是该Ubuntu LTS的,目前支持的是Ubuntu 22.04,所以,这也是我一直以来没有正常使用YCM的原因,因为我们的开发环境的VM都是相对比较低的CentOS发行版,最近才开始尝试基于Docker Images来构建稳定的开发环境。

Runtime Min Version Recommended Version (full support) Python
Vim 8.2.3995 9.0.214 3.8
Neovim 0.5 Vim 9.0.214 3.8

编译ycmd过程中需要支持C++17,所以关于编译版本要求:

Compiler Current Min
GCC 8
Clang 7
MSVC 15.7 (VS 2017)

除了以上的相关依赖的版本要求,还需要注意相关事项:

  • 通过源码编译Python版本的时候,需要开启--enable-shared
  • YCM需要CMake版本 >= 3.13;
  • 针对C/C++的语义补全,需要clang/clangd,Clang如果使用自定义的系统版本,需要Clang 17.0.1+的版本;

安装问题记录

首先声明我使用的Image是CentOS 8.4.2105的镜像。

我的YCM安装完成后,发现只有基于identifier completer标识符补全的功能,根本没有semantic completer基于语义的提示,例如C++中输入./->都没有相关的提示,按照README说明,安装后,应该自动会有semantic completion的功能。Sad。

通过:YcmDiags并没有任何错误,通过:YcmDebugInfo可以看到如下:

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
Printing YouCompleteMe debug information...
-- Resolve completions: Up front
-- Client logfile: /tmp/ycm_2llhah3r.log
-- Server Python interpreter: /usr/local/bin/python3
-- Server Python version: 3.11.8
-- Server has Clang support compiled in: True
-- Clang version: clang version 12.0.1 (Red Hat 12.0.1-4.module_el8.5.0+1025+93159d6c)
-- No extra configuration file found
-- C-family completer debug information:
-- Clangd not running
-- Clangd executable: ['/root/.vim/plugged/YouCompleteMe/third_party/ycmd/third_party/clangd/output/bin/clangd', '-header-insertion-decorators=0', '-resource-dir=/root/.vim/plugged/YouCompleteMe/third_party/ycmd/third_party/clang/lib/clang/17.0.1'
, '-limit-results=500']
-- Clangd logfiles:
-- /tmp/clangd_stderrrw71uww1.log
-- Clangd Server State: Dead
-- Clangd Project Directory: /data/vgserver_proj/server
-- Clangd Open Workspaces: {'/data/vgserver_proj/server'}
-- Clangd Settings: {}
-- Clangd Compilation Command: False
-- Server running at: http://127.0.0.1:38472
-- Server process ID: 16941
-- Server logfiles:
-- /tmp/ycmd_38472_stdout_y2vxaox6.log
-- /tmp/ycmd_38472_stderr_q2_94tk0.log
-- Semantic highlighting supported: True
-- Virtual text supported: True
-- Popup windows supported: True

通过:YcmDebugInfo可以看到相关启动服务的日志文件,看一下Clangd的错误日志,/tmp/clangd_stderrrw71uww1.log,打开果然有错误,如下,还是YCM默认继承的clangd引擎依赖的glibc版本相对较高,尝试用系统发行版支持的clang试试

1
2
3
/root/.vim/plugged/YouCompleteMe/third_party/ycmd/third_party/clangd/output/bin/clangd: /lib64/libm.so.6: version `GLIBC_2.29' not found (required by /root/.vim/plugged/YouCompleteMe/third_party/ycmd/third_party/clangd/output/bin/clangd)
/root/.vim/plugged/YouCompleteMe/third_party/ycmd/third_party/clangd/output/bin/clangd: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.26' not found (required by /root/.vim/plugged/YouCompleteMe/third_party/ycmd/third_party/clangd/output/bin/clang>
~

如下,在.vimrc中配置使用系统自带的clangd,

1
let g:ycm_clangd_binary_path = '/bin/clangd'

但是打开cpp文件时,又报了新的错误,如下:

1
NoExtraConfDetected: No .ycm_extra_conf.py file detected, so no compile flags are available. Thus no semantic support for C/C++/ObjC/ObjC++. Go READ THE DOCS *NOW*, DON'T file a bug report.

且通过:YcmDebugInfo发现,并没有使用clangd,只是使用libclang进行semantic completion,且对应的日志中提示: Clangd at /bin/clangd is out-of-date

1
2
6 2024-03-21 15:34:54,499 - INFO - Adding buffer identifiers for file: /data/linux_namespace.cpp                          
7 2024-03-21 15:34:54,531 - ERROR - Clangd at /bin/clangd is out-of-date 8 2024-03-21 15:34:54,531 - INFO - Completion config: 50, detailing -1 candiates

最终在YCM的README中,发现:如果使用自定义的clangd或者libclang,当前YCM需要clang 17.0.1的版本。真是无语…

接下来就开始了clang的编译之旅:

参考LLVM的官方手册源码构建发行版本,进行构建

1
2
3
4
5
$ git clone --depth=1 --branch=llvmorg-17.0.1 https://github.com/llvm/llvm-project.git
$ cd llvm-project
$ mkdir build
$ cd build
$ cmake -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ../llvm

构建完成后,在.vimrc中设置使用自定义的clangd版本配置:

1
let g:ycm_clangd_binary_path = '/usr/local/bin/clangd'

升级clang到17.0.1后,YCM终于可以支持semantic completion了,在vim打开cpp文件后,可以看到clangd正常启动,如下:

1
/usr/local/bin/clangd -header-insertion-decorators=0 -limit-results=500

但是打开clangd的错误日志发现有报错,如下:

1
2
E[20:19:08.190] VFS: failed to set CWD to /data/home/walkerdu/vgserver_proj/server/build/framework/base: No such file or directory
19 E[20:19:08.200] VFS: failed to set CWD to /data/home/walkerdu/vgserver_proj/server/build/framework/base: No such file or directory

这里比较奇怪的是,我是在Container中安装的环境,并没有这个目录,为什么clangd会去尝试切换这个目录呢?仔细观察发现,这是项目的build路径,我挂载到Container的volume中的目录在VM中有构建过整个项目,所以clangd尝试解析build中的构建过程来进行semantic completion,所以发现了问题,这里删除后就不会有这个报错了。

配置说明

YouCompleteMe插件安装完成后,默认的配置选项都会被设置为一个合理的值,一般不需要额外设置,详细可见:~/.vim/plugged/YouCompleteMe/plugin/youcompleteme.vim,比较重要的一些选项如下:

  • g:ycm_min_num_of_chars_for_completion

输入几个字符后,触发自动补全提示,默认是2,我改为了1

1
let g:ycm_min_num_of_chars_for_completion = 1
  • g:ycm_min_num_identifier_candidate_chars

自动补全提示中最少的字符个数,默认为0,表示不限制

1
let g:ycm_min_num_identifier_candidate_chars = 0
  • g:ycm_max_num_candidates

基于语义自动补全的最大候选个数,默认为50,官方建议不要设置为0或者超过100,因为这样会影响性能

1
let g:ycm_max_num_candidates = 50
  • g:ycm_max_num_candidates_to_detail
1
let g:ycm_max_num_candidates_to_detail = 0
  • g:ycm_max_num_identifier_candidates

基于标识符引擎自动补全的最大提示数量,默认10,官方建议不要设置为0或者超过100,因为这样会影响性能

1
let g:ycm_max_num_identifier_candidates = 10
  • g:ycm_auto_trigger

是否启用自动补全,默认开启,如果关闭,会关闭YCM的identifier completer标识符补全器semantic completer 语义补全器

1
let g:ycm_auto_trigger = 1

如果你只想关闭YCM的identifier completer,但是保留器semantic completer,那么可以通过设置g:ycm_min_num_identifier_candidate_chars为一个很大的值来实现,例如:

1
g:ycm_min_num_identifier_candidate_chars = 999
  • g:ycm_filetype_whitelist & g:ycm_filetype_blacklist

用来控制YCM生效的文件类型列表,配置是一个vim的字典,key是各类文件类型,例如pythoncpp,默认g:ycm_filetype_whitelist包含所有的文件,g:ycm_filetype_blacklist配置一些不生效的文件,具体可以在vim中通过:h filetype查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let g:ycm_filetype_whitelist = {'*': 1}
let g:ycm_filetype_blacklist = {
\ 'tagbar': 1
\ 'notes': 1,
\ 'markdown': 1,
\ 'netrw': 1,
\ 'unite': 1,
\ 'text': 1,
\ 'vimwiki': 1,
\ 'pandoc': 1,
\ 'infolog': 1,
\ 'leaderf': 1,
\ 'mail': 1
\}

BTW,vim通过set ft?来查看一个文件的类型名字

  • g:ycm_filetype_specific_completion_to_disable

此选项控制 YCM semantic completer语义补全引擎应该为哪些 Vim 文件类型关闭。默认配置:

1
2
3
let g:ycm_filetype_specific_completion_to_disable =
\ get( g:, 'ycm_filetype_specific_completion_to_disable',
\ { 'gitcommit': 1 } )

开箱即用的Vim镜像

之前为什么一直没有折腾Vim的插件就是因为,插件各种依赖,开发环境的经常变更,导致维护比较困难,后来接触到Docker后,这个问题很大程度上可以被解决,所以构建了一个基于CentOS的Vim镜像(Ubuntu的还没来得及),集成了基本的插件,大家有兴趣可以下下来试试。

基于CentOS的Vim镜像的构建文件和上下文都在这个Repo:develop-vim-centos

develop-vim-centos的仓库里面包含基础的.vimrc配置,包含了上面介绍所有插件和配置

然后为了方便开箱即用,vim的所有插件已经全部下载和初始化了,可以直接在Docker Hub中下载,地址如下:

1
2
3
4
5
# 环境初始化ok的vim镜像,运行后需要执行:PlusInstall 来安装所有vim插件
docker pull walkerdu/develop-vim-centos:v1

# 初始化后的vim镜像,开箱即用(建议使用这个)
docker pull walkerdu/develop-vim-centos:v1-init-complete

@Update :2024-04-07

抽空解决了Ubuntu 22.04在CentOS VM上运行目录权限的问题(看文章后面),构建了基于基于Ubuntu 22.04 LTS的Vim镜像。构建文件和上下文都在这个Repo:develop-vim-ubuntu

可以通过如下地址直接开箱即用,vim的所有插件已经全部下载和初始化了:

1
2
3
4
5
# 环境初始化ok的vim镜像,运行后需要执行:PlusInstall 来安装所有vim插件
docker pull walkerdu/develop-vim-ubuntu:v1

# 初始化后的vim镜像,开箱即用(建议使用这个)
docker pull walkerdu/develop-vim-ubuntu:v1-init-complete

镜像构建中的问题

基于ubuntu22.04构建的Container中(VM是CentOS 7.2)居然连根目录的读权限都没有,奔溃了,但是可以查看默认的当前目录,且写文件也没有问题,这就很奇怪了

1
2
3
4
root@ac941745381f:/# ls /
ls: cannot access '/': Operation not permitted
root@ac941745381f:/# ls -a
. .. .dockerenv bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var

搜索了一下,和这个Issue:https://github.com/docker-library/openjdk/issues/465比较像,我的base的OS也是CentOS Linux release 7.2 (Final)的,然后使用的Ubuntu:22.04的基础镜像,按照Issue的说法,我要升级 Docker, runc, and likely libseccomp,这就蛋疼了。

根据下面这两个帖子,应该是Docker Image采用的Linux 发行版本的内核比较新,使用了比较新的内核特性,但是旧版本的Docker,runc没有能够做到兼容,这里主要还是Linux较新版本的内核采用了libseccomp新版本支持的特性,但是旧版本的Docker,runc不支持的原因。

https://stackoverflow.com/questions/70714357

https://stackoverflow.com/questions/69786640

那其实是找到问题所在了,使用--privileged选项可以解决这个权限的问题,对于开发容器来说,算是先解决了,升级runc的成本有点高,先这样吧。

上面的问题解决后,下面的问题也不再发生了

官方ubuntu镜像在apt-get update的时候提示

1
2
Err:1 http://security.ubuntu.com/ubuntu jammy-security InRelease    
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 871920D1991BC93C

全部换成阿里的软件源,有同样的问题

apt-get update --allow-insecure-repositories

然后安装的时候通过apt-get install --no-install-recommends来绕过公钥验证完成安装。